Added a ton of JS Doc

pull/1013/head
Zef Hemel 2024-08-07 13:27:25 +02:00
parent e29352556b
commit cafd001214
27 changed files with 612 additions and 197 deletions

View File

@ -8,7 +8,6 @@ import plugAssetBundle from "../dist/plug_asset_bundle.json" with {
import { AssetBundle, type AssetJson } from "../lib/asset_bundle/bundle.ts";
import { determineDatabaseBackend } from "../server/db_backend.ts";
import type { SpaceServerConfig } from "../server/instance.ts";
import { runPlug } from "../cmd/plug_run.ts";
import { PrefixedKvPrimitives } from "$lib/data/prefixed_kv_primitives.ts";
import { sleep } from "$lib/async.ts";
@ -73,19 +72,6 @@ export async function serveCommand(
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
const enableSpaceScript = Deno.env.get("SB_SPACE_SCRIPT") !== "off";
const configs = new Map<string, SpaceServerConfig>();
configs.set("*", {
hostname,
namespace: "*",
auth: userCredentials,
authToken: Deno.env.get("SB_AUTH_TOKEN"),
syncOnly,
readOnly,
shellBackend: backendConfig,
enableSpaceScript,
pagesPath: folder,
});
const plugAssets = new AssetBundle(plugAssetBundle as AssetJson);
if (readOnly) {
@ -137,9 +123,16 @@ export async function serveCommand(
baseKvPrimitives,
keyFile: options.key,
certFile: options.cert,
configs,
auth: userCredentials,
authToken: Deno.env.get("SB_AUTH_TOKEN"),
syncOnly,
readOnly,
shellBackend: backendConfig,
enableSpaceScript,
pagesPath: folder,
});
httpServer.start();
await httpServer.start();
// Wait in an infinite loop (to keep the HTTP server running, only cancelable via Ctrl+C or other signal)
while (true) {

View File

@ -1,6 +1,7 @@
{
"name": "@silverbulletmd/silverbullet",
"version": "0.0.1",
"version": "0.9.0",
"description": "Silverbullet is a Personal Knowledge Management System",
"exports": {
"./syscall": "./plug-api/syscall.ts",
"./syscalls": "./plug-api/syscalls.ts",

View File

@ -136,7 +136,9 @@ export async function extractFrontmatter(
return data;
}
// Updates the front matter of a markdown document and returns the text as a rendered string
/**
* Updates the front matter of a markdown document and returns the text as a rendered string
*/
export async function prepareFrontmatterDispatch(
tree: ParseTree,
data: string | Record<string, any>,

View File

@ -1,4 +1,9 @@
// Compares two objects deeply
/**
* Performs a deep comparison of two objects, returning true if they are equal
* @param a first object
* @param b second object
* @returns
*/
export function deepEqual(a: any, b: any): boolean {
if (a === b) {
return true;
@ -40,7 +45,10 @@ export function deepEqual(a: any, b: any): boolean {
return false;
}
// Converts a Date object to a date string in the format YYYY-MM-DD if it just contains a date (and no significant time), or a full ISO string otherwise
/**
* Converts a Date object to a date string in the format YYYY-MM-DD if it just contains a date (and no significant time), or a full ISO string otherwise
* @param d the date to convert
*/
export function cleanStringDate(d: Date): string {
// If no significant time, return a date string only
if (
@ -54,9 +62,13 @@ export function cleanStringDate(d: Date): string {
}
}
// Processes a JSON (typically coming from parse YAML frontmatter) in two ways:
// 1. Expands property names in an object containing a .-separated path
// 2. Converts dates to strings in sensible ways
/**
* Processes a JSON (typically coming from parse YAML frontmatter) in two ways:
* 1. Expands property names in an object containing a .-separated path
* 2. Converts dates to strings in sensible ways
* @param a
* @returns
*/
export function cleanupJSON(a: any): any {
if (!a) {
return a;

View File

@ -4,6 +4,9 @@ import {
renderToText,
} from "@silverbulletmd/silverbullet/lib/tree";
/**
* Strips markdown from a ParseTree
*/
export function stripMarkdown(
tree: ParseTree,
): string {

View File

@ -1,3 +1,14 @@
/**
* Represents a reference to a page, with optional position, anchor and header.
*/
export type PageRef = {
page: string;
pos?: number | { line: number; column: number };
anchor?: string;
header?: string;
meta?: boolean;
};
/**
* Checks if a name looks like a full path (with a file extension), is not a conflicted file and not a search page.
*/
@ -6,6 +17,9 @@ export function looksLikePathWithExtension(name: string): boolean {
!name.startsWith("🔍 ");
}
/**
* Checks if a name looks like a full path (with a file extension), is not a conflicted file and not a search page.
*/
export function validatePageName(name: string) {
// Page can not be empty and not end with a file extension (e.g. "bla.md")
if (name === "") {
@ -19,19 +33,16 @@ export function validatePageName(name: string) {
}
}
export type PageRef = {
page: string;
pos?: number | { line: number; column: number };
anchor?: string;
header?: string;
meta?: boolean;
};
const posRegex = /@(\d+)$/;
const linePosRegex = /@[Ll](\d+)(?:[Cc](\d+))?$/; // column is optional, implicit 1
const anchorRegex = /\$([a-zA-Z\.\-\/]+[\w\.\-\/]*)$/;
const headerRegex = /#([^#]*)$/;
/**
* Parses a page reference string into a PageRef object.
* @param name the name of the page reference to parse
* @returns the parsed PageRef object
*/
export function parsePageRef(name: string): PageRef {
// Normalize the page name
if (name.startsWith("[[") && name.endsWith("]]")) {
@ -71,6 +82,11 @@ export function parsePageRef(name: string): PageRef {
return pageRef;
}
/**
* The inverse of parsePageRef, encodes a PageRef object into a string.
* @param pageRef the page reference to encode
* @returns a string representation of the page reference
*/
export function encodePageRef(pageRef: PageRef): string {
let name = pageRef.page;
if (pageRef.pos) {

View File

@ -12,6 +12,6 @@ if (typeof self === "undefined") {
}
// Late binding syscall
export function syscall(name: string, ...args: any[]) {
export function syscall(name: string, ...args: any[]): Promise<any> {
return (globalThis as any).syscall(name, ...args);
}

View File

@ -1,6 +1,13 @@
import { base64DecodeDataUrl } from "../../lib/crypto.ts";
import { syscall } from "../syscall.ts";
/**
* Reads an asset embedded in a plug (via the `assets` field in the plug manifest).
* @param plugName name of the plug to read asset from
* @param name name of the asset to read
* @param encoding either "utf8" or "dataurl"
* @returns the content of the asset in the requested encoding
*/
export async function readAsset(
plugName: string,
name: string,

View File

@ -5,14 +5,28 @@ import { syscall } from "../syscall.ts";
* Generally should only be used to set some client-specific states, such as preferences.
*/
/**
* Sets a value in the client store.
* @param key the key to set
* @param value the value to set
*/
export function set(key: string, value: any): Promise<void> {
return syscall("clientStore.set", key, value);
}
/**
* Gets a value from the client store.
* @param key the key to get
* @returns the value associated with the key
*/
export function get(key: string): Promise<any> {
return syscall("clientStore.get", key);
}
/**
* Deletes a value from the client store.
* @param key the key to delete
*/
export function del(key: string): Promise<void> {
return syscall("clientStore.delete", key);
}

View File

@ -1,6 +1,13 @@
import type { CodeWidgetContent } from "../types.ts";
import { syscall } from "../syscall.ts";
/**
* Renders a code widget.
* @param lang the language of the fenced code block
* @param body the body of the code to render
* @param pageName the name of the page the code widget appears on
* @returns the rendered code widget content
*/
export function render(
lang: string,
body: string,
@ -9,7 +16,9 @@ export function render(
return syscall("codeWidget.render", lang, body, pageName);
}
// Refresh all code widgets on the page that support it
/**
* Refreshes all code widgets on the page that support it.
*/
export function refreshAll(): Promise<void> {
return syscall("codeWidget.refreshAll");
}

View File

@ -1,30 +1,67 @@
import { syscall } from "../syscall.ts";
import type { KV, KvKey, KvQuery } from "../types.ts";
/**
* Exposes a key value story with query capabilities.
*/
/**
* Sets a value in the key value store.
* @param key the key to set
* @param value the value to set
*/
export function set(key: KvKey, value: any): Promise<void> {
return syscall("datastore.set", key, value);
}
/**
* Sets multiple values in the key value store.
* @param kvs the key value pairs to set
*/
export function batchSet(kvs: KV[]): Promise<void> {
return syscall("datastore.batchSet", kvs);
}
export function get(key: KvKey): Promise<any> {
/**
* Gets a value from the key value store.
* @param key the key to get
* @returns the value associated with the key (or undefined if not found)
*/
export function get(key: KvKey): Promise<any | undefined> {
return syscall("datastore.get", key);
}
/**
* Gets multiple values from the key value store.
* @param keys the keys to get
* @returns the values associated with the keys (or undefined if not found)
*/
export function batchGet(keys: KvKey[]): Promise<(any | undefined)[]> {
return syscall("datastore.batchGet", keys);
}
/**
* Deletes a value from the key value store.
* @param key the key to delete
*/
export function del(key: KvKey): Promise<void> {
return syscall("datastore.delete", key);
}
/**
* Deletes multiple values from the key value store.
* @param keys the keys to delete
*/
export function batchDel(keys: KvKey[]): Promise<void> {
return syscall("datastore.batchDelete", keys);
}
/**
* Queries the key value store.
* @param query the query to run
* @param variables the variables that can be referenced inside the query
* @returns the results of the query
*/
export function query(
query: KvQuery,
variables: Record<string, any> = {},
@ -32,6 +69,11 @@ export function query(
return syscall("datastore.query", query, variables);
}
/**
* Queries the key value store and deletes all matching items
* @param query the query to run
* @param variables the variables that can be referenced inside the query
*/
export function queryDelete(
query: KvQuery,
variables?: Record<string, any>,
@ -39,6 +81,10 @@ export function queryDelete(
return syscall("datastore.queryDelete", query, variables);
}
/**
* Lists all functions currently defined and available for use in queries
* @returns the names of all functions in the key value store
*/
export function listFunctions(): Promise<string[]> {
return syscall("datastore.listFunctions");
}

View File

@ -1,5 +1,12 @@
import { syscall } from "../syscall.ts";
/**
* Exposes various debugging utilities.
*/
/**
* Completely wipes the client state, both cached files as well as databases (best effort)
*/
export function resetClient(): Promise<void> {
return syscall("debug.resetClient");
}

View File

@ -3,14 +3,22 @@ import { syscall } from "../syscall.ts";
import type { PageRef } from "../lib/page_ref.ts";
import type { FilterOption } from "@silverbulletmd/silverbullet/type/client";
/**
* Exposes various editor utilities.
* Important: These syscalls are only available in the client.
*/
/**
* Returns the name of the page currently open in the editor.
* @returns the current page name
*/
export function getCurrentPage(): Promise<string> {
return syscall("editor.getCurrentPage");
}
export function setPage(newName: string): Promise<void> {
return syscall("editor.setPage", newName);
}
/**
* Returns the full text of the currently open page
*/
export function getText(): Promise<string> {
return syscall("editor.getText");
}
@ -23,22 +31,42 @@ export function setText(newText: string): Promise<void> {
return syscall("editor.setText", newText);
}
/**
* Returns the position (in # of characters from the beginning of the file) of the cursor in the editor
*/
export function getCursor(): Promise<number> {
return syscall("editor.getCursor");
}
/**
* Returns the line number and column number of the cursor in the editor
*/
export function getSelection(): Promise<{ from: number; to: number }> {
return syscall("editor.getSelection");
}
/**
* Sets the position of the cursor in the editor
* @param from the start position of the selection
* @param to the end position of the selection
*/
export function setSelection(from: number, to: number): Promise<void> {
return syscall("editor.setSelection", from, to);
}
/**
* Forces a save of the current page
*/
export function save(): Promise<void> {
return syscall("editor.save");
}
/**
* Navigates to the specified page reference
* @param pageRef the page reference to navigate to
* @param replaceState whether to replace the current history state in the browser history
* @param newWindow whether to open the page in a new window
*/
export function navigate(
pageRef: PageRef,
replaceState = false,
@ -47,28 +75,49 @@ export function navigate(
return syscall("editor.navigate", pageRef, replaceState, newWindow);
}
/**
* Opens the page navigator
* @param mode the mode to open the navigator in
*/
export function openPageNavigator(
mode: "page" | "meta" | "all" = "page",
): Promise<void> {
return syscall("editor.openPageNavigator", mode);
}
/**
* Opens the command palette
*/
export function openCommandPalette(): Promise<void> {
return syscall("editor.openCommandPalette");
}
/**
* Force reloads the current page
*/
export function reloadPage(): Promise<void> {
return syscall("editor.reloadPage");
}
/**
* Force reloads the browser UI
*/
export function reloadUI(): Promise<void> {
return syscall("editor.reloadUI");
}
/**
* Reloads the config and commands, also in the server
*/
export function reloadConfigAndCommands(): Promise<void> {
return syscall("editor.reloadConfigAndCommands");
}
/**
* Opens the specified URL in the browser
* @param url the URL to open
* @param existingWindow whether to open the URL in an existing window
*/
export function openUrl(url: string, existingWindow = false): Promise<void> {
return syscall("editor.openUrl", url, existingWindow);
}
@ -82,11 +131,20 @@ export function goHistory(delta: number): Promise<void> {
return syscall("editor.goHistory", delta);
}
// Force the client to download the file in dataUrl with filename as file name
/**
* Force the client to download the file in dataUrl with filename as file name
* @param filename the name of the file to download
* @param dataUrl the dataUrl of the file to download
*/
export function downloadFile(filename: string, dataUrl: string): Promise<void> {
return syscall("editor.downloadFile", filename, dataUrl);
}
/**
* Triggers the browser's native file upload dialog/popup
* @param accept the file types to accept
* @param capture the capture mode for the file input
*/
export function uploadFile(
accept?: string,
capture?: string,
@ -94,6 +152,11 @@ export function uploadFile(
return syscall("editor.uploadFile", accept, capture);
}
/**
* Shows a flash notification to the user (top right corner)
* @param message the message to show
* @param type the type of notification to show
*/
export function flashNotification(
message: string,
type: "info" | "error" = "info",
@ -101,6 +164,13 @@ export function flashNotification(
return syscall("editor.flashNotification", message, type);
}
/**
* Exposes a filter box UI (similar to the page navigator and command palette)
* @param label the label to show left of the input box
* @param options the options to show and to filter on
* @param helpText the help text to show below the input box
* @param placeHolder the placeholder text to show in the input box
*/
export function filterBox(
label: string,
options: FilterOption[],
@ -110,6 +180,13 @@ export function filterBox(
return syscall("editor.filterBox", label, options, helpText, placeHolder);
}
/**
* Shows a panel in the editor
* @param id the location of the panel to show
* @param mode the mode or "size" of the panel
* @param html the html content of the panel
* @param script the script content of the panel
*/
export function showPanel(
id: "lhs" | "rhs" | "bhs" | "modal",
mode: number,
@ -119,16 +196,31 @@ export function showPanel(
return syscall("editor.showPanel", id, mode, html, script);
}
/**
* Hides a panel in the editor
* @param id the location of the panel to hide
*/
export function hidePanel(
id: "lhs" | "rhs" | "bhs" | "modal",
): Promise<void> {
return syscall("editor.hidePanel", id);
}
/**
* Insert text at the specified position into the editor
* @param text the text to insert
* @param pos
*/
export function insertAtPos(text: string, pos: number): Promise<void> {
return syscall("editor.insertAtPos", text, pos);
}
/**
* Replace the text in the specified range in the editor
* @param from the start position of the range
* @param to the end position of the range
* @param text the text to replace with
*/
export function replaceRange(
from: number,
to: number,
@ -137,10 +229,21 @@ export function replaceRange(
return syscall("editor.replaceRange", from, to, text);
}
/**
* Move the cursor to the specified position in the editor
* @param pos the position to move the cursor to
* @param center whether to center the cursor in the editor after moving
*/
export function moveCursor(pos: number, center = false): Promise<void> {
return syscall("editor.moveCursor", pos, center);
}
/**
* Move the cursor to the specified line and column in the editor
* @param line the line number to move the cursor to
* @param column the column number to move the cursor to
* @param center whether to center the cursor in the editor after moving
*/
export function moveCursorToLine(
line: number,
column = 1,
@ -149,14 +252,27 @@ export function moveCursorToLine(
return syscall("editor.moveCursorToLine", line, column, center);
}
/**
* Insert text at the cursor position in the editor
* @param text the text to insert
*/
export function insertAtCursor(text: string): Promise<void> {
return syscall("editor.insertAtCursor", text);
}
/**
* Dispatch a CodeMirror transaction: https://codemirror.net/docs/ref/#state.Transaction
*/
export function dispatch(change: any): Promise<void> {
return syscall("editor.dispatch", change);
}
/**
* Prompt the user for text input
* @param message the message to show in the prompt
* @param defaultValue a default value pre-filled in the prompt
* @returns
*/
export function prompt(
message: string,
defaultValue = "",
@ -164,62 +280,112 @@ export function prompt(
return syscall("editor.prompt", message, defaultValue);
}
/**
* Prompt the user for confirmation
* @param message the message to show in the confirmation dialog
* @returns
*/
export function confirm(
message: string,
): Promise<boolean> {
return syscall("editor.confirm", message);
}
/**
* Get the value of a UI option
* @param key the key of the UI option to get
* @returns
*/
export function getUiOption(key: string): Promise<any> {
return syscall("editor.getUiOption", key);
}
/**
* Set the value of a UI option
* @param key the key of the UI option to set
* @param value the value to set the UI option to
*/
export function setUiOption(key: string, value: any): Promise<void> {
return syscall("editor.setUiOption", key, value);
}
// Vim specific
export function vimEx(exCommand: string): Promise<any> {
return syscall("editor.vimEx", exCommand);
}
// Folding
/**
* Perform a fold at the current cursor position
*/
export function fold(): Promise<void> {
return syscall("editor.fold");
}
/**
* Perform an unfold at the current cursor position
*/
export function unfold(): Promise<void> {
return syscall("editor.unfold");
}
/**
* Toggle the fold at the current cursor position
*/
export function toggleFold(): Promise<void> {
return syscall("editor.toggleFold");
}
/**
* Fold all code blocks in the editor
*/
export function foldAll(): Promise<void> {
return syscall("editor.foldAll");
}
/**
* Unfold all code blocks in the editor
*/
export function unfoldAll(): Promise<void> {
return syscall("editor.unfoldAll");
}
// Undo/redo
/**
* Perform an undo operation of the last edit in the editor
*/
export function undo(): Promise<void> {
return syscall("editor.undo");
}
/**
* Perform a redo operation of the last undo in the editor
*/
export function redo(): Promise<void> {
return syscall("editor.redo");
}
/**
* Open the editor's native search panel
*/
export function openSearchPanel(): Promise<void> {
return syscall("editor.openSearchPanel");
}
/**
* Copy the specified data to the clipboard
* @param data the data to copy
*/
export function copyToClipboard(data: string | Blob): Promise<void> {
return syscall("editor.copyToClipboard", data);
}
/**
* Delete the current line in the editor
*/
export function deleteLine(): Promise<void> {
return syscall("editor.deleteLine");
}
// Vim-mode specific syscalls
/**
* Execute a Vim ex command
* @param exCommand the ex command to execute
*/
export function vimEx(exCommand: string): Promise<any> {
return syscall("editor.vimEx", exCommand);
}

View File

@ -1,5 +1,14 @@
import { syscall } from "../syscall.ts";
/**
* Triggers an event on the SilverBullet event bus.
* This can be used to implement an RPC-style system too, because event handlers can return values,
* which are then accumulated in an array and returned to the caller.
* @param eventName the name of the event to trigger
* @param data payload to send with the event
* @param timeout optional timeout in milliseconds to wait for a response
* @returns an array of responses from the event handlers (if any)
*/
export function dispatchEvent(
eventName: string,
data: any,
@ -24,6 +33,10 @@ export function dispatchEvent(
});
}
/**
* List all events currently registered (listened to) on the SilverBullet event bus.
* @returns an array of event names
*/
export function listEvents(): Promise<string[]> {
return syscall("event.list");
}

View File

@ -1,8 +1,14 @@
import { syscall } from "../syscall.ts";
/**
* Validates a JSON object against a JSON schema.
* @param schema the JSON schema to validate against
* @param object the JSON object to validate
* @returns an error message if the object is invalid, or undefined if it is valid
*/
export function validateObject(
schema: any,
object: any,
): Promise<any> {
): Promise<string | undefined> {
return syscall("jsonschema.validateObject", schema, object);
}

View File

@ -14,6 +14,10 @@ export function parseLanguage(
return syscall("language.parseLanguage", language, code);
}
/**
* Lists all supported languages in fenced code blocks
* @returns a list of all supported languages
*/
export function listLanguages(): Promise<string[]> {
return syscall("language.listLanguages");
}

View File

@ -1,7 +1,11 @@
import { syscall } from "../syscall.ts";
import type { ParseTree } from "../lib/tree.ts";
/**
* Parses a piece of markdown text into a ParseTree.
* @param text the markdown text to parse
* @returns a ParseTree representation of the markdown text
*/
export function parseMarkdown(text: string): Promise<ParseTree> {
return syscall("markdown.parseMarkdown", text);
}

View File

@ -1,22 +1,50 @@
import { syscall } from "../syscall.ts";
import type { MQStats } from "../types.ts";
/**
* Implements a simple Message Queue system.
*/
/**
* Sends a message to a queue.
* @param queue the name of the queue to send the message to
* @param body the body of the message to send
*/
export function send(queue: string, body: any): Promise<void> {
return syscall("mq.send", queue, body);
}
/**
* Sends a batch of messages to a queue.
* @param queue the name of the queue
* @param bodies the bodies of the messages to send
*/
export function batchSend(queue: string, bodies: any[]): Promise<void> {
return syscall("mq.batchSend", queue, bodies);
}
/**
* Acknowledges a message from a queue, in case it needs to be explicitly acknowledged.
* @param queue the name of the queue the message came from
* @param id the id of the message to acknowledge
*/
export function ack(queue: string, id: string): Promise<void> {
return syscall("mq.ack", queue, id);
}
/**
* Acknowledges a batch of messages from a queue, in case they need to be explicitly acknowledged.
* @param queue the name of the queue the messages came from
* @param ids the ids of the messages to acknowledge
*/
export function batchAck(queue: string, ids: string[]): Promise<void> {
return syscall("mq.batchAck", queue, ids);
}
/**
* Retrieves stats on a particular queue.
* @param queue the name of the queue
*/
export function getQueueStats(queue: string): Promise<MQStats> {
return syscall("mq.getQueueStats", queue);
}

View File

@ -1,5 +1,11 @@
import { syscall } from "../syscall.ts";
/**
* Runs a shell command.
* @param cmd the command to run
* @param args the arguments to pass to the command
* @returns the stdout, stderr, and exit code of the command
*/
export function run(
cmd: string,
args: string[],

View File

@ -1,37 +1,74 @@
import { syscall } from "../syscall.ts";
import type { AttachmentMeta, FileMeta, PageMeta } from "../types.ts";
export function listPages(unfiltered = false): Promise<PageMeta[]> {
return syscall("space.listPages", unfiltered);
/**
* Lists all pages (files ending in .md) in the space.
* @param unfiltered
* @returns a list of all pages in the space represented as PageMeta objects
*/
export function listPages(): Promise<PageMeta[]> {
return syscall("space.listPages");
}
/**
* Get metadata for a page in the space.
* @param name the name of the page to get metadata for
* @returns the metadata for the page
*/
export function getPageMeta(name: string): Promise<PageMeta> {
return syscall("space.getPageMeta", name);
}
/**
* Read a page from the space as text.
* @param name the name of the page to read
* @returns the text of the page
*/
export function readPage(
name: string,
): Promise<string> {
return syscall("space.readPage", name);
}
/**
* Write a page to the space.
* @param name the name of the page to write
* @param text the text of the page to write
* @returns the metadata for the written page
*/
export function writePage(name: string, text: string): Promise<PageMeta> {
return syscall("space.writePage", name, text);
}
/**
* Delete a page from the space.
* @param name the name of the page to delete
*/
export function deletePage(name: string): Promise<void> {
return syscall("space.deletePage", name);
}
/**
* List all plugs in the space.
* @returns a list of all plugs in the space represented as FileMeta objects
*/
export function listPlugs(): Promise<FileMeta[]> {
return syscall("space.listPlugs");
}
/**
* Lists all attachments in the space (all files not ending in .md).
* @returns a list of all attachments in the space represented as AttachmentMeta objects
*/
export function listAttachments(): Promise<AttachmentMeta[]> {
return syscall("space.listAttachments");
}
/**
* Get metadata for an attachment in the space.
* @param name the path of the attachment to get metadata for
* @returns the metadata for the attachment
*/
export function getAttachmentMeta(name: string): Promise<AttachmentMeta> {
return syscall("space.getAttachmentMeta", name);
}
@ -68,19 +105,40 @@ export function deleteAttachment(name: string): Promise<void> {
return syscall("space.deleteAttachment", name);
}
// FS
// Lower level-file operations
/**
* List all files in the space (pages, attachments and plugs).
* @returns a list of all files in the space represented as FileMeta objects
*/
export function listFiles(): Promise<FileMeta[]> {
return syscall("space.listFiles");
}
/**
* Read a file from the space as a Uint8Array.
* @param name the name of the file to read
* @returns the data of the file
*/
export function readFile(name: string): Promise<Uint8Array> {
return syscall("space.readFile", name);
}
/**
* Get metadata for a file in the space.
* @param name the name of the file to get metadata for
* @returns the metadata for the file
*/
export function getFileMeta(name: string): Promise<FileMeta> {
return syscall("space.getFileMeta", name);
}
/**
* Write a file to the space.
* @param name the name of the file to write
* @param data the data of the file to write
* @returns the metadata for the written file
*/
export function writeFile(
name: string,
data: Uint8Array,
@ -88,6 +146,10 @@ export function writeFile(
return syscall("space.writeFile", name, data);
}
/**
* Delete a file from the space.
* @param name the name of the file to delete
*/
export function deleteFile(name: string): Promise<void> {
return syscall("space.deleteFile", name);
}

View File

@ -1,17 +1,34 @@
import { syscall } from "../syscall.ts";
/**
* Syscalls that interact with the sync engine (when the client runs in Sync mode)
*/
/**
* Checks if a sync is currently in progress
*/
export function isSyncing(): Promise<boolean> {
return syscall("sync.isSyncing");
}
/**
* Checks if an initial sync has completed
*/
export function hasInitialSyncCompleted(): Promise<boolean> {
return syscall("sync.hasInitialSyncCompleted");
}
/**
* Actively schedules a file to be synced. Sync will happen by default too, but this prioritizes the file.
* @param path the path to the file to sync
*/
export function scheduleFileSync(path: string): Promise<void> {
return syscall("sync.scheduleFileSync", path);
}
/**
* Schedules a sync of without waiting for the usual sync interval.
*/
export function scheduleSpaceSync(): Promise<number> {
return syscall("sync.scheduleSpaceSync");
}

View File

@ -4,6 +4,12 @@ import type { ParseTree } from "../lib/tree.ts";
import { syscall } from "../syscall.ts";
import type { Config } from "../../type/config.ts";
/**
* Invoke a plug function
* @param name a string representing the name of the function to invoke ("plug.functionName")
* @param args arguments to pass to the function
* @returns
*/
export function invokeFunction(
name: string,
...args: any[]
@ -11,20 +17,38 @@ export function invokeFunction(
return syscall("system.invokeFunction", name, ...args);
}
// Only available on the client
/**
* Invoke a client command by name
* Note: only available on the client
* @param name name of the command
* @param args arguments to pass to the command
*/
export function invokeCommand(name: string, args?: string[]): Promise<any> {
return syscall("system.invokeCommand", name, args);
}
// Only available on the client
export function listCommands(): Promise<{ [key: string]: CommandDef }> {
/**
* Lists all commands available
* @returns a map of all available commands
*/
export function listCommands(): Promise<Record<string, CommandDef>> {
return syscall("system.listCommands");
}
/**
* Lists all syscalls available
* @returns a list of all available syscalls
*/
export function listSyscalls(): Promise<SyscallMeta[]> {
return syscall("system.listSyscalls");
}
/**
* Invoke a space function by name
* @param name a string representing the name of the function to invoke
* @param args arguments to pass to the function
* @returns the value returned by the function
*/
export function invokeSpaceFunction(
name: string,
...args: any[]
@ -32,6 +56,9 @@ export function invokeSpaceFunction(
return syscall("system.invokeSpaceFunction", name, ...args);
}
/**
* Applies attribute extractors to a ParseTree
*/
export function applyAttributeExtractors(
tags: string[],
text: string,
@ -43,6 +70,7 @@ export function applyAttributeExtractors(
/**
* Loads a particular space configuration key (or all of them when no key is spacified)
* @param key the key to load, when not specified, all keys are loaded
* @param defaultValue the default value to return when the key is not found
* @returns either the value of the key or all keys as a Record<string, any>
*/
export async function getSpaceConfig(
@ -52,23 +80,39 @@ export async function getSpaceConfig(
return (await syscall("system.getSpaceConfig", key)) ?? defaultValue;
}
/**
* Trigger a reload of all plugs
* @returns
*/
export function reloadPlugs(): Promise<void> {
return syscall("system.reloadPlugs");
}
/**
* Trigger an explicit reload of the configuration
* @returns the new configuration
*/
export function reloadConfig(): Promise<Config> {
return syscall("system.reloadConfig");
}
// Returns what runtime environment this plug is run in, e.g. "server" or "client" can be undefined, which would mean a hybrid environment (such as mobile)
/**
* Returns what runtime environment this plug is run in, e.g. "server" or "client" can be undefined, which would mean a hybrid environment (such as mobile)
*/
export function getEnv(): Promise<string | undefined> {
return syscall("system.getEnv");
}
/**
* Returns the current mode of the system, either "ro" (read-only) or "rw" (read-write)
*/
export function getMode(): Promise<"ro" | "rw"> {
return syscall("system.getMode");
}
/**
* Returns the SilverBullet version
*/
export function getVersion(): Promise<string> {
return syscall("system.getVersion");
}

View File

@ -1,11 +1,12 @@
import type { AST } from "@silverbulletmd/silverbullet/lib/tree";
import { syscall } from "../syscall.ts";
/**
* Renders
* @param template
* @param obj
* @param globals
* @returns
* Renders a template with the given object and globals.
* @param template the text of the template to render
* @param obj the object to render the template with
* @param globals the globals to render the template with
* @returns the rendered template
*/
export function renderTemplate(
template: string,
@ -15,8 +16,13 @@ export function renderTemplate(
return syscall("template.renderTemplate", template, obj, globals);
}
/**
* Parses a template into an AST.
* @param template the text of the template to parse
* @returns an AST representation of the template
*/
export function parseTemplate(
template: string,
): Promise<string> {
): Promise<AST> {
return syscall("template.parseTemplate", template);
}

View File

@ -1,11 +1,21 @@
import { syscall } from "../syscall.ts";
/**
* Parses a YAML string into a JavaScript object.
* @param text the YAML text to parse
* @returns a JavaScript object representation of the YAML text
*/
export function parse(
text: string,
): Promise<any> {
return syscall("yaml.parse", text);
}
/**
* Converts a JavaScript object into a YAML string.
* @param obj the object to stringify
* @returns a YAML string representation of the object
*/
export function stringify(
obj: any,
): Promise<string> {

View File

@ -1,6 +1,6 @@
import { deleteCookie, getCookie, setCookie } from "hono/helper.ts";
import { cors } from "hono/middleware.ts";
import { type Context, Hono, type HonoRequest, validator } from "hono/mod.ts";
import { type Context, Hono, validator } from "hono/mod.ts";
import type { AssetBundle } from "$lib/asset_bundle/bundle.ts";
import type {
EndpointRequest,
@ -8,8 +8,7 @@ import type {
FileMeta,
} from "@silverbulletmd/silverbullet/types";
import type { ShellRequest } from "@silverbulletmd/silverbullet/type/rpc";
import { SpaceServer } from "./instance.ts";
import type { SpaceServerConfig } from "./instance.ts";
import { SpaceServer } from "./space_server.ts";
import type { KvPrimitives } from "$lib/data/kv_primitives.ts";
import { PrefixedKvPrimitives } from "$lib/data/prefixed_kv_primitives.ts";
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
@ -33,7 +32,15 @@ export type ServerOptions = {
certFile?: string;
keyFile?: string;
configs: Map<string, SpaceServerConfig>;
// Enable username/password auth
auth?: { user: string; pass: string };
// Additional API auth token
authToken?: string;
pagesPath: string;
shellBackend: string;
syncOnly: boolean;
readOnly: boolean;
enableSpaceScript: boolean;
};
export class HttpServer {
@ -46,11 +53,11 @@ export class HttpServer {
keyFile: string | undefined;
certFile: string | undefined;
spaceServers = new Map<string, Promise<SpaceServer>>();
// Available after start()
spaceServer!: SpaceServer;
baseKvPrimitives: KvPrimitives;
configs: Map<string, SpaceServerConfig>;
constructor(options: ServerOptions) {
constructor(private options: ServerOptions) {
this.app = new Hono();
this.clientAssetBundle = options.clientAssetBundle;
this.plugAssetBundle = options.plugAssetBundle;
@ -59,60 +66,6 @@ export class HttpServer {
this.keyFile = options.keyFile;
this.certFile = options.certFile;
this.baseKvPrimitives = options.baseKvPrimitives;
this.configs = options.configs;
}
async bootSpaceServer(config: SpaceServerConfig): Promise<SpaceServer> {
const spaceServer = new SpaceServer(
config,
this.plugAssetBundle,
new PrefixedKvPrimitives(this.baseKvPrimitives, [
config.namespace,
]),
);
await spaceServer.init();
return spaceServer;
}
determineConfig(req: HonoRequest): [string, SpaceServerConfig] {
const url = new URL(req.url);
let hostname = url.host; // hostname:port
// First try a full match
let config = this.configs.get(hostname);
if (config) {
return [hostname, config];
}
// Then rip off the port and try again
hostname = hostname.split(":")[0];
config = this.configs.get(hostname);
if (config) {
return [hostname, config];
}
// If all else fails, try the wildcard
config = this.configs.get("*");
if (config) {
return ["*", config];
}
throw new Error(`No space server config found for hostname ${hostname}`);
}
ensureSpaceServer(req: HonoRequest): Promise<SpaceServer> {
const [matchedHostname, config] = this.determineConfig(req);
const spaceServer = this.spaceServers.get(matchedHostname);
if (spaceServer) {
return spaceServer;
}
// And then boot the thing, async
const spaceServerPromise = this.bootSpaceServer(config);
// But immediately write the promise to the map so that we don't boot it twice
this.spaceServers.set(matchedHostname, spaceServerPromise);
return spaceServerPromise;
}
// Replaces some template variables in index.html in a rather ad-hoc manner, but YOLO
@ -179,19 +132,26 @@ export class HttpServer {
);
}
start() {
async start() {
// Serve static files (javascript, css, html)
this.serveStatic();
this.serveCustomEndpoints();
this.addAuth();
this.addFsRoutes();
// Boot space server
this.spaceServer = new SpaceServer(
this.options,
this.plugAssetBundle,
new PrefixedKvPrimitives(this.baseKvPrimitives, ["*"]), // * for backwards compatibility reasons
);
await this.spaceServer.init();
// Fallback, serve the UI index.html
this.app.use("*", async (c) => {
const spaceServer = await this.ensureSpaceServer(c.req);
this.app.use("*", (c) => {
const url = new URL(c.req.url);
const pageName = decodeURI(url.pathname.slice(1));
return this.renderHtmlPage(spaceServer, pageName, c);
return this.renderHtmlPage(this.spaceServer, pageName, c);
});
this.abortController = new AbortController();
@ -221,16 +181,16 @@ export class HttpServer {
// Custom endpoints can be defined in the server
serveCustomEndpoints() {
this.app.use("/_/*", async (ctx) => {
const spaceServer = await this.ensureSpaceServer(ctx.req);
const req = ctx.req;
const url = new URL(req.url);
if (!spaceServer.serverSystem) {
if (!this.spaceServer.serverSystem) {
return ctx.text("No server system available", 500);
}
try {
const path = url.pathname.slice(2); // Remove the /_
const responses: EndpointResponse[] = await spaceServer.serverSystem
const responses: EndpointResponse[] = await this.spaceServer
.serverSystem
.eventHook.dispatchEvent(`http:request:${path}`, {
fullPath: url.pathname,
path,
@ -278,17 +238,17 @@ export class HttpServer {
}
serveStatic() {
this.app.use("*", async (c, next) => {
this.app.use("*", (c, next): Promise<void | Response> => {
const req = c.req;
const spaceServer = await this.ensureSpaceServer(req);
const url = new URL(req.url);
// console.log("URL", url);
if (
url.pathname === "/"
) {
// Serve the UI (index.html)
const indexPage = parsePageRef(spaceServer.config?.indexPage!).page;
return this.renderHtmlPage(spaceServer, indexPage, c);
const indexPage =
parsePageRef(this.spaceServer.config?.indexPage!).page;
return this.renderHtmlPage(this.spaceServer, indexPage, c);
}
try {
const assetName = url.pathname.slice(1);
@ -301,7 +261,7 @@ export class HttpServer {
utcDateString(this.clientAssetBundle.getMtime(assetName)) &&
assetName !== "service_worker.js"
) {
return c.body(null, 304);
return Promise.resolve(c.body(null, 304));
}
c.status(200);
c.header("Content-type", this.clientAssetBundle.getMimeType(assetName));
@ -327,18 +287,19 @@ export class HttpServer {
"{{CONFIG_HASH}}",
base64Encode(
JSON.stringify([
spaceServer.syncOnly,
spaceServer.readOnly,
spaceServer.enableSpaceScript,
this.spaceServer.syncOnly,
this.spaceServer.readOnly,
this.spaceServer.enableSpaceScript,
]),
),
);
}
return c.body(data);
return Promise.resolve(c.body(data));
} // else e.g. HEAD, OPTIONS, don't send body
} catch {
return next();
}
return Promise.resolve();
});
}
@ -381,16 +342,14 @@ export class HttpServer {
const url = new URL(c.req.url);
const { username, password } = req.valid("form");
const spaceServer = await this.ensureSpaceServer(req);
const {
user: expectedUser,
pass: expectedPassword,
} = spaceServer.auth!;
} = this.spaceServer.auth!;
if (username === expectedUser && password === expectedPassword) {
// Generate a JWT and set it as a cookie
const jwt = await spaceServer.jwtIssuer.createJWT(
const jwt = await this.spaceServer.jwtIssuer.createJWT(
{ username },
authenticationExpirySeconds,
);
@ -417,8 +376,7 @@ export class HttpServer {
// Check auth
this.app.use("*", async (c, next) => {
const req = c.req;
const spaceServer = await this.ensureSpaceServer(req);
if (!spaceServer.auth && !spaceServer.authToken) {
if (!this.spaceServer.auth && !this.spaceServer.authToken) {
// Auth disabled in this config, skip
return next();
}
@ -435,12 +393,12 @@ export class HttpServer {
if (!excludedPaths.includes(url.pathname)) {
const authCookie = getCookie(c, authCookieName(host));
if (!authCookie && spaceServer.authToken) {
if (!authCookie && this.spaceServer.authToken) {
// Attempt Bearer Authorization based authentication
const authHeader = req.header("Authorization");
if (authHeader && authHeader.startsWith("Bearer ")) {
const authToken = authHeader.slice("Bearer ".length);
if (authToken === spaceServer.authToken) {
if (authToken === this.spaceServer.authToken) {
// All good, let's proceed
return next();
} else {
@ -455,10 +413,11 @@ export class HttpServer {
console.log("Unauthorized access, redirecting to auth page");
return redirectToAuth();
}
const { user: expectedUser } = spaceServer.auth!;
const { user: expectedUser } = this.spaceServer.auth!;
try {
const verifiedJwt = await spaceServer.jwtIssuer.verifyAndDecodeJWT(
const verifiedJwt = await this.spaceServer.jwtIssuer
.verifyAndDecodeJWT(
authCookie,
);
if (verifiedJwt.username !== expectedUser) {
@ -490,12 +449,11 @@ export class HttpServer {
// File list
this.app.get("/index.json", async (c) => {
const req = c.req;
const spaceServer = await this.ensureSpaceServer(req);
if (req.header("X-Sync-Mode")) {
// Only handle direct requests for a JSON representation of the file list
const files = await spaceServer.spacePrimitives.fetchFileList();
const files = await this.spaceServer.spacePrimitives.fetchFileList();
return c.json(files, 200, {
"X-Space-Path": spaceServer.pagesPath,
"X-Space-Path": this.spaceServer.pagesPath,
});
} else {
// Otherwise, redirect to the UI
@ -514,11 +472,10 @@ export class HttpServer {
// RPC shell
this.app.post("/.rpc/shell", async (c) => {
const req = c.req;
const spaceServer = await this.ensureSpaceServer(req);
const body = await req.json();
try {
const shellCommand: ShellRequest = body;
const shellResponse = await spaceServer.shellBackend.handle(
const shellResponse = await this.spaceServer.shellBackend.handle(
shellCommand,
);
return c.json(shellResponse);
@ -533,15 +490,14 @@ export class HttpServer {
const req = c.req;
const syscall = req.param("syscall")!;
const plugName = req.param("plugName")!;
const spaceServer = await this.ensureSpaceServer(req);
const body = await req.json();
try {
if (spaceServer.syncOnly) {
if (this.spaceServer.syncOnly) {
return c.text("Sync only mode, no syscalls allowed", 400);
}
const args: string[] = body;
try {
const result = await spaceServer.system!.syscall(
const result = await this.spaceServer.system!.syscall(
{ plug: plugName === "_" ? undefined : plugName },
syscall,
args,
@ -566,7 +522,6 @@ export class HttpServer {
this.app.get(filePathRegex, async (c) => {
const req = c.req;
const name = req.param("path")!;
const spaceServer = await this.ensureSpaceServer(req);
console.log("Requested file", name);
if (
@ -627,12 +582,12 @@ export class HttpServer {
try {
if (req.header("X-Get-Meta")) {
// Getting meta via GET request
const fileData = await spaceServer.spacePrimitives.getFileMeta(
const fileData = await this.spaceServer.spacePrimitives.getFileMeta(
name,
);
return c.text("", 200, this.fileMetaToHeaders(fileData));
}
const fileData = await spaceServer.spacePrimitives.readFile(name);
const fileData = await this.spaceServer.spacePrimitives.readFile(name);
const lastModifiedHeader = new Date(fileData.meta.lastModified)
.toUTCString();
if (
@ -652,8 +607,7 @@ export class HttpServer {
async (c) => {
const req = c.req;
const name = req.param("path")!;
const spaceServer = await this.ensureSpaceServer(req);
if (spaceServer.readOnly) {
if (this.spaceServer.readOnly) {
return c.text("Read only mode, no writes allowed", 405);
}
console.log("Writing file", name);
@ -670,7 +624,7 @@ export class HttpServer {
const body = await req.arrayBuffer();
try {
const meta = await spaceServer.spacePrimitives.writeFile(
const meta = await this.spaceServer.spacePrimitives.writeFile(
name,
new Uint8Array(body),
);
@ -683,8 +637,7 @@ export class HttpServer {
).delete(async (c) => {
const req = c.req;
const name = req.param("path")!;
const spaceServer = await this.ensureSpaceServer(req);
if (spaceServer.readOnly) {
if (this.spaceServer.readOnly) {
return c.text("Read only mode, no writes allowed", 405);
}
console.log("Deleting file", name);
@ -693,7 +646,7 @@ export class HttpServer {
return c.text("Forbidden", 403);
}
try {
await spaceServer.spacePrimitives.deleteFile(name);
await this.spaceServer.spacePrimitives.deleteFile(name);
return c.text("OK");
} catch (e: any) {
console.error("Error deleting attachment", e);
@ -707,8 +660,7 @@ export class HttpServer {
proxyPathRegex,
async (c, next) => {
const req = c.req;
const spaceServer = await this.ensureSpaceServer(req);
if (spaceServer.readOnly) {
if (this.spaceServer.readOnly) {
return c.text("Read only mode, no federation proxy allowed", 405);
}
let url = req.param("uri")!.slice(1);

View File

@ -1,5 +1,5 @@
import type { SpaceServerConfig } from "./instance.ts";
import type { ShellRequest, ShellResponse } from "../type/rpc.ts";
import type { ServerOptions } from "./http_server.ts";
/**
* Configuration via environment variables:
@ -7,12 +7,12 @@ import type { ShellRequest, ShellResponse } from "../type/rpc.ts";
*/
export function determineShellBackend(
spaceServerConfig: SpaceServerConfig,
serverOptions: ServerOptions,
): ShellBackend {
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
switch (backendConfig) {
case "local":
return new LocalShell(spaceServerConfig.pagesPath);
return new LocalShell(serverOptions.pagesPath);
default:
console.info(
"Running in shellless mode, meaning shell commands are disabled",

View File

@ -22,20 +22,7 @@ import { determineShellBackend, NotSupportedShell } from "./shell_backend.ts";
import type { ShellBackend } from "./shell_backend.ts";
import { determineStorageBackend } from "./storage_backend.ts";
import type { Config } from "../type/config.ts";
export type SpaceServerConfig = {
hostname: string;
namespace: string;
// Enable username/password auth
auth?: { user: string; pass: string };
// Additional API auth token
authToken?: string;
pagesPath: string;
shellBackend: string;
syncOnly: boolean;
readOnly: boolean;
enableSpaceScript: boolean;
};
import type { ServerOptions } from "./http_server.ts";
// Equivalent of Client on the server
export class SpaceServer implements ConfigContainer {
@ -58,24 +45,24 @@ export class SpaceServer implements ConfigContainer {
enableSpaceScript: boolean;
constructor(
config: SpaceServerConfig,
options: ServerOptions,
private plugAssetBundle: AssetBundle,
private kvPrimitives: KvPrimitives,
) {
this.pagesPath = config.pagesPath;
this.hostname = config.hostname;
this.auth = config.auth;
this.authToken = config.authToken;
this.syncOnly = config.syncOnly;
this.readOnly = config.readOnly;
this.pagesPath = options.pagesPath;
this.hostname = options.hostname;
this.auth = options.auth;
this.authToken = options.authToken;
this.syncOnly = options.syncOnly;
this.readOnly = options.readOnly;
this.config = defaultConfig;
this.enableSpaceScript = config.enableSpaceScript;
this.enableSpaceScript = options.enableSpaceScript;
this.jwtIssuer = new JWTIssuer(kvPrimitives);
this.shellBackend = config.readOnly
this.shellBackend = options.readOnly
? new NotSupportedShell() // No shell for read only mode
: determineShellBackend(config);
: determineShellBackend(options);
}
async init() {