1
0

Work on client modes

This commit is contained in:
Zef Hemel 2023-08-29 21:17:29 +02:00
parent 5ff1a8bae3
commit 9a005f26b5
54 changed files with 594 additions and 302 deletions

View File

@ -3,7 +3,7 @@ import { DiskSpacePrimitives } from "../common/spaces/disk_space_primitives.ts";
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
import { Application } from "../server/deps.ts";
import { sleep } from "../common/async_util.ts";
import { sleep } from "$sb/lib/async.ts";
import { ServerSystem } from "../server/server_system.ts";
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";

View File

@ -13,8 +13,8 @@ import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import { S3SpacePrimitives } from "../server/spaces/s3_space_primitives.ts";
import { Authenticator } from "../server/auth.ts";
import { JSONKVStore } from "../plugos/lib/kv_store.json_file.ts";
import { sleep } from "../common/async_util.ts";
import { ServerSystem } from "../server/server_system.ts";
import { sleep } from "$sb/lib/async.ts";
import { SilverBulletHooks } from "../common/manifest.ts";
import { System } from "../plugos/system.ts";
@ -26,10 +26,9 @@ export async function serveCommand(
auth?: string;
cert?: string;
key?: string;
// Thin client mode
thinClient?: boolean;
reindex?: boolean;
db?: string;
serverProcessing?: boolean;
},
folder?: string,
) {
@ -38,7 +37,6 @@ export async function serveCommand(
const port = options.port ||
(Deno.env.get("SB_PORT") && +Deno.env.get("SB_PORT")!) || 3000;
const thinClientMode = options.thinClient || Deno.env.has("SB_THIN_CLIENT");
let dbFile = options.db || Deno.env.get("SB_DB_FILE") || ".silverbullet.db";
const app = new Application();
@ -86,9 +84,12 @@ To allow outside connections, pass -L 0.0.0.0 as a flag, and put a TLS terminato
);
let system: System<SilverBulletHooks> | undefined;
if (thinClientMode) {
if (options.serverProcessing) {
// Enable server-side processing
dbFile = path.resolve(folder, dbFile);
console.log(`Running in thin client mode, keeping state in ${dbFile}`);
console.log(
`Running in server-processing mode, keeping state in ${dbFile}`,
);
const serverSystem = new ServerSystem(spacePrimitives, dbFile, app);
await serverSystem.init();
spacePrimitives = serverSystem.spacePrimitives;
@ -132,15 +133,20 @@ To allow outside connections, pass -L 0.0.0.0 as a flag, and put a TLS terminato
authStore.loadString(envAuth);
}
const httpServer = new HttpServer(spacePrimitives!, app, system, {
hostname,
port: port,
pagesPath: folder!,
clientAssetBundle: new AssetBundle(clientAssetBundle as AssetJson),
authenticator,
keyFile: options.key,
certFile: options.cert,
});
const httpServer = new HttpServer(
spacePrimitives!,
app,
system,
{
hostname,
port: port,
pagesPath: folder!,
clientAssetBundle: new AssetBundle(clientAssetBundle as AssetJson),
authenticator,
keyFile: options.key,
certFile: options.cert,
},
);
await httpServer.start();
// Wait in an infinite loop (to keep the HTTP server running, only cancelable via Ctrl+C or other signal)

View File

@ -1,32 +0,0 @@
export function throttle(func: () => void, limit: number) {
let timer: any = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
func();
timer = null;
}, limit);
}
};
}
// race for promises returns first promise that resolves
export function race<T>(promises: Promise<T>[]): Promise<T> {
return new Promise((resolve, reject) => {
for (const p of promises) {
p.then(resolve, reject);
}
});
}
export function timeout(ms: number): Promise<never> {
return new Promise((_resolve, reject) =>
setTimeout(() => {
reject(new Error("timeout"));
}, ms)
);
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@ -115,9 +115,9 @@ export class EventedSpacePrimitives implements SpacePrimitives {
console.error("Error dispatching page:saved event", e);
});
}
if (name.startsWith("_plug/") && name.endsWith(".plug.js")) {
await this.dispatchEvent("plug:changed", name);
}
// if (name.startsWith("_plug/") && name.endsWith(".plug.js")) {
// await this.dispatchEvent("plug:changed", name);
// }
return newMeta;
}

View File

@ -0,0 +1,26 @@
import { assertEquals } from "../../test_deps.ts";
import { PromiseQueue, sleep } from "./async.ts";
Deno.test("PromiseQueue test", async () => {
const q = new PromiseQueue();
let r1RanFirst = false;
const r1 = q.runInQueue(async () => {
await sleep(10);
r1RanFirst = true;
// console.log("1");
return 1;
});
const r2 = q.runInQueue(async () => {
// console.log("2");
await sleep(4);
return 2;
});
assertEquals(await Promise.all([r1, r2]), [1, 2]);
assertEquals(r1RanFirst, true);
let wasRun = false;
await q.runInQueue(async () => {
await sleep(4);
wasRun = true;
});
assertEquals(wasRun, true);
});

69
plug-api/lib/async.ts Normal file
View File

@ -0,0 +1,69 @@
export function throttle(func: () => void, limit: number) {
let timer: any = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
func();
timer = null;
}, limit);
}
};
}
// race for promises returns first promise that resolves
export function race<T>(promises: Promise<T>[]): Promise<T> {
return new Promise((resolve, reject) => {
for (const p of promises) {
p.then(resolve, reject);
}
});
}
export function timeout(ms: number): Promise<never> {
return new Promise((_resolve, reject) =>
setTimeout(() => {
reject(new Error("timeout"));
}, ms)
);
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export class PromiseQueue {
private queue: {
fn: () => Promise<any>;
resolve: (value: any) => void;
reject: (error: any) => void;
}[] = [];
private running = false;
runInQueue(fn: () => Promise<any>): Promise<any> {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
if (!this.running) {
this.run();
}
});
}
private async run(): Promise<void> {
if (this.queue.length === 0) {
this.running = false;
return;
}
this.running = true;
const { fn, resolve, reject } = this.queue.shift()!;
try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
}
this.run(); // Continue processing the next promise in the queue
}
}

View File

@ -0,0 +1,20 @@
import { assertEquals } from "../../test_deps.ts";
import { PromiseQueue, sleep } from "./async.ts";
Deno.test("PromiseQueue test", async () => {
const q = new PromiseQueue();
let r1RanFirst = false;
const r1 = q.runInQueue(async () => {
await sleep(10);
r1RanFirst = true;
console.log("1");
return 1;
});
const r2 = q.runInQueue(async () => {
console.log("2");
await sleep(4);
return 2;
});
console.log(await Promise.all([r1, r2]));
assertEquals(r1RanFirst, true);
});

View File

@ -1,4 +1,4 @@
import { sleep } from "../../common/async_util.ts";
import { sleep } from "$sb/lib/async.ts";
import { DenoKvMQ } from "./mq.deno_kv.ts";
Deno.test("Deno MQ", async () => {

View File

@ -41,14 +41,20 @@ export class DenoKvMQ implements MessageQueue {
}
async batchSend(queue: string, bodies: any[]): Promise<void> {
const results = await Promise.all(
bodies.map((body) => this.kv.enqueue([queue, body])),
);
for (const result of results) {
for (const body of bodies) {
const result = await this.kv.enqueue([queue, body]);
if (!result.ok) {
throw result;
}
}
// const results = await Promise.all(
// bodies.map((body) => this.kv.enqueue([queue, body])),
// );
// for (const result of results) {
// if (!result.ok) {
// throw result;
// }
// }
}
async send(queue: string, body: any): Promise<void> {
const result = await this.kv.enqueue([queue, body]);

View File

@ -1,7 +1,7 @@
import { IDBKeyRange, indexedDB } from "https://esm.sh/fake-indexeddb@4.0.2";
import { DexieMQ } from "./mq.dexie.ts";
import { assertEquals } from "../../test_deps.ts";
import { sleep } from "../../common/async_util.ts";
import { sleep } from "$sb/lib/async.ts";
Deno.test("Dexie MQ", async () => {
const mq = new DexieMQ("test", indexedDB, IDBKeyRange);

View File

@ -10,7 +10,7 @@ import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
import type { PageMeta } from "../../web/types.ts";
import { isFederationPath } from "$sb/lib/resolve.ts";
import { MQMessage } from "$sb/types.ts";
import { sleep } from "../../common/async_util.ts";
import { sleep } from "$sb/lib/async.ts";
const directiveUpdateQueueName = "directiveUpdateQueue";

View File

@ -14,7 +14,7 @@ export async function brokenLinksCommand() {
if (tree.type === "WikiLinkPage") {
// Add the prefix in the link text
const [pageName] = tree.children![0].text!.split("@");
if (pageName.startsWith("💭 ")) {
if (pageName.startsWith("!")) {
return true;
}
if (
@ -31,7 +31,7 @@ export async function brokenLinksCommand() {
}
if (tree.type === "PageRef") {
const pageName = tree.children![0].text!.slice(2, -2);
if (pageName.startsWith("💭 ")) {
if (pageName.startsWith("!")) {
return true;
}
if (!allPagesMap.has(pageName)) {

View File

@ -1,7 +0,0 @@
import { editor } from "$sb/syscalls.ts";
export async function setThinClient(def: any) {
console.log("Setting thin client to", def.value);
await editor.setUiOption("thinClientMode", def.value);
await editor.reloadUI();
}

View File

@ -1,6 +1,4 @@
name: editor
requiredPermissions:
- fetch
syntax:
NakedURL:
firstCharacters:
@ -59,6 +57,10 @@ functions:
name: "Navigate: Home"
key: "Alt-h"
page: ""
moveToPos:
path: "./editor.ts:moveToPosCommand"
command:
name: "Navigate: Move Cursor to Position"
# Text editing commands
quoteSelectionCommand:
@ -113,12 +115,8 @@ functions:
centerCursor:
path: "./editor.ts:centerCursorCommand"
command:
name: "Editor: Center Cursor"
name: "Navigate: Center Cursor"
key: "Ctrl-Alt-l"
moveToPos:
path: "./editor.ts:moveToPosCommand"
command:
name: "Editor: Move Cursor to Position"
# Debug commands
parseCommand:
@ -197,18 +195,6 @@ functions:
command:
name: "Broken Links: Show"
# Client mode
enableThinClient:
path: ./client.ts:setThinClient
command:
name: "Client: Enable Thin Client"
value: true
disableThinClient:
path: ./client.ts:setThinClient
command:
name: "Client: Disable Thin Client"
value: false
# Random stuff
statsCommand:
path: ./stats.ts:statsCommand

View File

@ -96,8 +96,6 @@ functions:
events:
- editor:complete
# Hashtags
indexTags:
path: "./tags.ts:indexTags"

View File

@ -11,7 +11,7 @@ import {
import { applyQuery } from "$sb/lib/query.ts";
import type { MQMessage } from "$sb/types.ts";
import { sleep } from "../../common/async_util.ts";
import { sleep } from "$sb/lib/async.ts";
// Key space:
// meta: => metaJson
@ -53,9 +53,7 @@ export async function processIndexQueue(messages: MQMessage[]) {
const name: string = message.body;
console.log(`Indexing page ${name}`);
const text = await space.readPage(name);
// console.log("Going to parse markdown");
const parsed = await markdown.parseMarkdown(text);
// console.log("Dispatching ;age:index");
await events.dispatchEvent("page:index", {
name,
tree: parsed,
@ -69,7 +67,7 @@ export async function clearPageIndex(page: string) {
}
export async function parseIndexTextRepublish({ name, text }: IndexEvent) {
console.log("Reindexing", name);
// console.log("Reindexing", name);
await events.dispatchEvent("page:index", {
name,
tree: await markdown.parseMarkdown(text),

View File

@ -2,8 +2,6 @@ name: search
functions:
indexPage:
path: search.ts:indexPage
# Only enable in client for now
# env: client
events:
- page:index

View File

@ -4,6 +4,7 @@ import { applyQuery } from "$sb/lib/query.ts";
import { editor, index, store } from "$sb/syscalls.ts";
import { BatchKVStore, SimpleSearchEngine } from "./engine.ts";
import { FileMeta } from "$sb/types.ts";
import { PromiseQueue } from "$sb/lib/async.ts";
const searchPrefix = "🔍 ";
@ -30,11 +31,16 @@ const ftsRevKvStore = new StoreKVStore("fts_rev:");
const engine = new SimpleSearchEngine(ftsKvStore, ftsRevKvStore);
export async function indexPage({ name, tree }: IndexTreeEvent) {
// Search indexing is prone to concurrency issues, so we queue all write operations
const promiseQueue = new PromiseQueue();
export function indexPage({ name, tree }: IndexTreeEvent) {
const text = renderToText(tree);
// console.log("Now FTS indexing", name);
await engine.deleteDocument(name);
await engine.indexDocument({ id: name, text });
return promiseQueue.runInQueue(async () => {
// console.log("Now FTS indexing", name);
await engine.deleteDocument(name);
await engine.indexDocument({ id: name, text });
});
}
export async function clearIndex() {
@ -42,8 +48,10 @@ export async function clearIndex() {
await store.deletePrefix("fts_rev:");
}
export async function pageUnindex(pageName: string) {
await engine.deleteDocument(pageName);
export function pageUnindex(pageName: string) {
return promiseQueue.runInQueue(() => {
return engine.deleteDocument(pageName);
});
}
export async function queryProvider({

View File

@ -68,7 +68,11 @@ export class HttpServer {
.replaceAll(
"{{SPACE_PATH}}",
this.options.pagesPath.replaceAll("\\", "\\\\"),
).replaceAll("{{THIN_CLIENT_MODE}}", this.system ? "on" : "off");
// );
).replaceAll(
"{{SUPPORT_ONLINE_MODE}}",
this.system ? "true" : "false",
);
}
async start() {

View File

@ -23,21 +23,22 @@ import { markdownSyscalls } from "../web/syscalls/markdown.ts";
import { spaceSyscalls } from "./syscalls/space.ts";
import { systemSyscalls } from "../web/syscalls/system.ts";
import { yamlSyscalls } from "../web/syscalls/yaml.ts";
import { Application, path } from "./deps.ts";
import { Application } from "./deps.ts";
import { sandboxFetchSyscalls } from "../plugos/syscalls/fetch.ts";
import { shellSyscalls } from "../plugos/syscalls/shell.deno.ts";
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import { DenoKvMQ } from "../plugos/lib/mq.deno_kv.ts";
import { base64EncodedDataUrl } from "../plugos/asset_bundle/base64.ts";
import { Plug } from "../plugos/plug.ts";
const fileListInterval = 30 * 1000; // 30s
export class ServerSystem {
system: System<SilverBulletHooks> = new System("server");
spacePrimitives!: SpacePrimitives;
private requeueInterval?: number;
kvStore?: DenoKVStore;
listInterval?: number;
denoKv!: Deno.Kv;
kvStore!: DenoKVStore;
listInterval?: number;
constructor(
private baseSpacePrimitives: SpacePrimitives,
@ -93,7 +94,7 @@ export class ServerSystem {
assetSyscalls(this.system),
yamlSyscalls(),
storeSyscalls(this.kvStore),
systemSyscalls(undefined as any, this.system),
systemSyscalls(this.system),
mqSyscalls(mq),
pageIndexCalls,
debugSyscalls(),
@ -136,34 +137,34 @@ export class ServerSystem {
text: new TextDecoder().decode(data.data),
});
}
if (path.startsWith("_plug/") && path.endsWith(".plug.js")) {
console.log("Plug updated, reloading:", path);
this.system.unload(path);
await this.loadPlugFromSpace(path);
}
})().catch(console.error);
});
}
async loadPlugs() {
const tempDir = await Deno.makeTempDir();
try {
for (const { name } of await this.spacePrimitives.fetchFileList()) {
if (name.endsWith(".plug.js")) {
const plugPath = path.join(tempDir, name);
await Deno.mkdir(path.dirname(plugPath), { recursive: true });
await Deno.writeFile(
plugPath,
(await this.spacePrimitives.readFile(name)).data,
);
await this.system.load(
new URL(`file://${plugPath}`),
createSandbox,
);
}
for (const { name } of await this.spacePrimitives.fetchFileList()) {
if (name.endsWith(".plug.js")) {
await this.loadPlugFromSpace(name);
}
} finally {
await Deno.remove(tempDir, { recursive: true });
}
}
async loadPlugFromSpace(path: string): Promise<Plug<SilverBulletHooks>> {
const plugJS = (await this.spacePrimitives.readFile(path)).data;
return this.system.load(
// Base64 encoding this to support `deno compile` mode
new URL(base64EncodedDataUrl("application/javascript", plugJS)),
createSandbox,
);
}
async close() {
clearInterval(this.requeueInterval);
clearInterval(this.listInterval);
await this.system.unloadAll();
}

View File

@ -45,16 +45,16 @@ await new Command()
"Path to TLS key",
)
.option(
"-t [type:boolean], --thin-client [type:boolean]",
"Enable thin-client mode",
"--no-server-processing [type:boolean]",
"Disable online mode (no server-side processing)",
)
.option(
"--reindex [type:boolean]",
"Reindex space on startup (applies to thin-mode only)",
"Reindex space on startup",
)
.option(
"--db <db:string>",
"Path to database file (applies to thin-mode only)",
"Path to database file",
)
.action(serveCommand)
// plug:compile

View File

@ -1,14 +1,15 @@
import { safeRun } from "../common/util.ts";
import { Client } from "./client.ts";
const thinClientMode = !!localStorage.getItem("thinClientMode");
const syncMode = window.silverBulletConfig.supportOnlineMode !== "true" ||
!!localStorage.getItem("syncMode");
safeRun(async () => {
console.log("Booting SilverBullet...");
const client = new Client(
document.getElementById("sb-root")!,
thinClientMode,
syncMode,
);
await client.init();
window.client = client;
@ -22,7 +23,7 @@ if (navigator.serviceWorker) {
.then(() => {
console.log("Service worker registered...");
});
if (!thinClientMode) {
if (syncMode) {
navigator.serviceWorker.ready.then((registration) => {
registration.active!.postMessage({
type: "config",

View File

@ -16,12 +16,17 @@ import { PathPageNavigator } from "./navigator.ts";
import { AppViewState, BuiltinSettings } from "./types.ts";
import type { AppEvent, CompleteEvent } from "../plug-api/app_event.ts";
import { throttle } from "../common/async_util.ts";
import { throttle } from "$sb/lib/async.ts";
import { PlugSpacePrimitives } from "../common/spaces/plug_space_primitives.ts";
import { IndexedDBSpacePrimitives } from "../common/spaces/indexeddb_space_primitives.ts";
import { FileMetaSpacePrimitives } from "../common/spaces/file_meta_space_primitives.ts";
import { EventedSpacePrimitives } from "../common/spaces/evented_space_primitives.ts";
import { pageSyncInterval, SyncService } from "./sync_service.ts";
import {
ISyncService,
NoSyncSyncService,
pageSyncInterval,
SyncService,
} from "./sync_service.ts";
import { simpleHash } from "../common/crypto.ts";
import { DexieKVStore } from "../plugos/lib/kv_store.dexie.ts";
import { SyncStatus } from "../common/spaces/sync.ts";
@ -47,6 +52,7 @@ declare global {
// Injected via index.html
silverBulletConfig: {
spaceFolderPath: string;
supportOnlineMode: string;
};
client: Client;
}
@ -75,7 +81,7 @@ export class Client {
// Track if plugs have been updated since sync cycle
fullSyncCompleted = false;
syncService: SyncService;
syncService: ISyncService;
settings!: BuiltinSettings;
kvStore: DexieKVStore;
mq: DexieMQ;
@ -88,7 +94,7 @@ export class Client {
constructor(
parent: Element,
private thinClientMode = false,
public syncMode = false,
) {
// Generate a semi-unique prefix for the database so not to reuse databases for different space paths
this.dbPrefix = "" + simpleHash(window.silverBulletConfig.spaceFolderPath);
@ -117,26 +123,26 @@ export class Client {
this.mq,
this.dbPrefix,
this.eventHook,
this.thinClientMode,
);
const localSpacePrimitives = this.initSpace();
this.syncService = new SyncService(
localSpacePrimitives,
this.plugSpaceRemotePrimitives,
this.kvStore,
this.eventHook,
(path) => {
// TODO: At some point we should remove the data.db exception here
return path !== "data.db" &&
// Exclude all plug space primitives paths
!this.plugSpaceRemotePrimitives.isLikelyHandled(path) ||
// Except federated ones
path.startsWith("!");
},
!this.thinClientMode,
);
this.syncService = this.syncMode
? new SyncService(
localSpacePrimitives,
this.plugSpaceRemotePrimitives,
this.kvStore,
this.eventHook,
(path) => {
// TODO: At some point we should remove the data.db exception here
return path !== "data.db" &&
// Exclude all plug space primitives paths
!this.plugSpaceRemotePrimitives.isLikelyHandled(path) ||
// Except federated ones
path.startsWith("!");
},
)
: new NoSyncSyncService(this.space);
this.ui = new MainUI(this);
this.ui.render(parent);
@ -243,14 +249,15 @@ export class Client {
Math.round(status.filesProcessed / status.totalFiles * 100),
);
});
this.syncService.spaceSync.on({
fileSynced: (meta, direction) => {
this.eventHook.addLocalListener(
"file:synced",
(meta: FileMeta, direction: string) => {
if (meta.name.endsWith(".md") && direction === "secondary->primary") {
// We likely polled the currently open page which trigggered a local update, let's update the editor accordingly
this.space.getPageMeta(meta.name.slice(0, -3));
}
},
});
);
}
private initNavigator() {
@ -337,7 +344,7 @@ export class Client {
let localSpacePrimitives: SpacePrimitives | undefined;
if (!this.thinClientMode) {
if (this.syncMode) {
localSpacePrimitives = new FilteredSpacePrimitives(
new FileMetaSpacePrimitives(
new EventedSpacePrimitives(

View File

@ -53,10 +53,9 @@ export class ClientSystem {
private mq: DexieMQ,
dbPrefix: string,
private eventHook: EventHook,
private thinClientMode: boolean,
) {
// Only set environment to "client" when running in thin client mode, otherwise we run everything locally (hybrid)
this.system = new System(thinClientMode ? "client" : undefined);
this.system = new System(client.syncMode ? undefined : "client");
this.system.addHook(this.eventHook);
@ -68,9 +67,11 @@ export class ClientSystem {
const cronHook = new CronHook(this.system);
this.system.addHook(cronHook);
if (thinClientMode) {
if (!client.syncMode) {
// In non-sync mode, proxy these to the server
this.indexSyscalls = indexProxySyscalls(client);
} else {
// In sync mode, run them locally
this.indexSyscalls = pageIndexSyscalls(
`${dbPrefix}_page_index`,
globalThis.indexedDB,
@ -83,7 +84,8 @@ export class ClientSystem {
this.system.addHook(this.codeWidgetHook);
// MQ hook
if (!this.thinClientMode) {
if (client.syncMode) {
// Process MQ messages locally
this.system.addHook(new MQHook(this.system, this.mq));
}
@ -103,19 +105,21 @@ export class ClientSystem {
this.slashCommandHook = new SlashCommandHook(this.client);
this.system.addHook(this.slashCommandHook);
this.eventHook.addLocalListener("plug:changed", async (fileName) => {
console.log("Plug updated, reloading:", fileName);
this.system.unload(fileName);
const plug = await this.system.load(
new URL(`/${fileName}`, location.href),
createSandbox,
this.client.settings.plugOverrides,
);
if ((plug.manifest! as Manifest).syntax) {
// If there are syntax extensions, rebuild the markdown parser immediately
this.updateMarkdownParser();
this.eventHook.addLocalListener("file:changed", async (path: string) => {
if (path.startsWith("_plug/") && path.endsWith(".plug.js")) {
console.log("Plug updated, reloading:", path);
this.system.unload(path);
const plug = await this.system.load(
new URL(`/${path}`, location.href),
createSandbox,
this.client.settings.plugOverrides,
);
if ((plug.manifest! as Manifest).syntax) {
// If there are syntax extensions, rebuild the markdown parser immediately
this.updateMarkdownParser();
}
this.plugsUpdated = true;
}
this.plugsUpdated = true;
});
// Debugging
@ -139,9 +143,11 @@ export class ClientSystem {
}
registerSyscalls() {
const storeCalls = this.thinClientMode
? storeProxySyscalls(this.client)
: storeSyscalls(this.kvStore);
const storeCalls = this.client.syncMode
// In sync mode handle locally
? storeSyscalls(this.kvStore)
// In non-sync mode proxy to server
: storeProxySyscalls(this.client);
// Slash command hook
this.slashCommandHook = new SlashCommandHook(this.client);
@ -153,11 +159,15 @@ export class ClientSystem {
eventSyscalls(this.eventHook),
editorSyscalls(this.client),
spaceSyscalls(this.client),
systemSyscalls(this.client, this.system),
systemSyscalls(this.system, this.client),
markdownSyscalls(buildMarkdown(this.mdExtensions)),
assetSyscalls(this.system),
yamlSyscalls(),
this.thinClientMode ? mqProxySyscalls(this.client) : mqSyscalls(this.mq),
this.client.syncMode
// In sync mode handle locally
? mqSyscalls(this.mq)
// In non-sync mode proxy to server
: mqProxySyscalls(this.client),
storeCalls,
this.indexSyscalls,
debugSyscalls(),

View File

@ -12,6 +12,7 @@ import { MiniEditor } from "./mini_editor.tsx";
export type ActionButton = {
icon: FunctionalComponent<FeatherProps>;
description: string;
class?: string;
callback: () => void;
href?: string;
};
@ -141,6 +142,7 @@ export function TopBar({
e.stopPropagation();
}}
title={actionButton.description}
className={actionButton.class}
>
<actionButton.icon size={18} />
</button>

View File

@ -12,6 +12,7 @@ export {
export {
Book as BookIcon,
Home as HomeIcon,
RefreshCw as RefreshCwIcon,
Terminal as TerminalIcon,
} from "https://esm.sh/preact-feather@4.2.1?external=preact";

View File

@ -10,6 +10,7 @@ import {
BookIcon,
HomeIcon,
preactRender,
RefreshCwIcon,
runScopeHandlers,
TerminalIcon,
useEffect,
@ -18,6 +19,7 @@ import {
import type { Client } from "./client.ts";
import { Panel } from "./components/panel.tsx";
import { h } from "./deps.ts";
import { async } from "https://cdn.skypack.dev/-/regenerator-runtime@v0.13.9-4Dxus9nU31cBsHxnWq2H/dist=es2020,mode=imports/optimized/regenerator-runtime.js";
export class MainUI {
viewState: AppViewState = initialViewState;
@ -202,6 +204,40 @@ export class MainUI {
editor.focus();
}}
actionButtons={[
...window.silverBulletConfig.supportOnlineMode === "true"
? [{
icon: RefreshCwIcon,
description: this.editor.syncMode
? "Currently in sync mode: switch to online mode"
: "Currently in online mode: switch to sync mode",
class: this.editor.syncMode ? "sb-enabled" : undefined,
callback: () => {
(async () => {
const newValue = !this.editor.syncMode;
if (newValue) {
if (
await this.editor.confirm(
"This will enable local sync. Are you sure?",
)
) {
localStorage.setItem("syncMode", "true");
location.reload();
}
} else {
if (
await this.editor.confirm(
"This will disable local sync. Are you sure?",
)
) {
localStorage.removeItem("syncMode");
location.reload();
}
}
})().catch(console.error);
},
}]
: [],
{
icon: HomeIcon,
description: `Go home (Alt-h)`,

View File

@ -34,12 +34,14 @@
};
window.silverBulletConfig = {
// These {{VARIABLES}} are replaced by http_server.ts
spaceFolderPath: "{{SPACE_PATH}}"
spaceFolderPath: "{{SPACE_PATH}}",
supportOnlineMode: "{{SUPPORT_ONLINE_MODE}}",
};
// But in case these variables aren't replaced by the server, fall back fully static mode (no sync)
if (window.silverBulletConfig.spaceFolderPath.includes("{{")) {
window.silverBulletConfig = {
spaceFolderPath: "",
supportOnlineMode: false,
};
}
</script>

View File

@ -89,6 +89,11 @@ self.addEventListener("fetch", (event: any) => {
return cachedResponse;
}
if (!fileContentTable) {
// Not initialzed yet, or in thin client mode, let's just proxy
return fetch(request);
}
const requestUrl = new URL(request.url);
const pathname = requestUrl.pathname;
@ -119,17 +124,12 @@ async function handleLocalFileRequest(
request: Request,
pathname: string,
): Promise<Response> {
if (!fileContentTable) {
// Not initialzed yet, or explicitly in sync mode (so direct server communication requested)
return fetch(request);
}
if (!db?.isOpen()) {
console.log("Detected that the DB was closed, reopening");
await db!.open();
}
const path = decodeURIComponent(pathname.slice(1));
const data = await fileContentTable.get(path);
const data = await fileContentTable!.get(path);
if (data) {
// console.log("Serving from space", path);
if (!data.meta) {

View File

@ -2,10 +2,11 @@ import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import { plugPrefix } from "../common/spaces/constants.ts";
import { safeRun } from "../common/util.ts";
import { AttachmentMeta, PageMeta } from "./types.ts";
import { throttle } from "../common/async_util.ts";
import { KVStore } from "../plugos/lib/kv_store.ts";
import { FileMeta } from "$sb/types.ts";
import { EventHook } from "../plugos/hooks/event.ts";
import { throttle } from "$sb/lib/async.ts";
const pageWatchInterval = 5000;

View File

@ -71,6 +71,10 @@
cursor: pointer;
}
.sb-actions button.sb-enabled {
color: var(--action-button-active-color);
}
.sb-actions button:hover {
color: var(--action-button-hover-color);
}

View File

@ -45,6 +45,7 @@ html {
--action-button-background-color: transparent;
--action-button-color: #292929;
--action-button-hover-color: #0772be;
--action-button-active-color: #0772be;
--editor-caret-color: black;
--editor-selection-background-color: #d7e1f6;
@ -159,6 +160,7 @@ html[data-theme="dark"] {
--action-button-background-color: transparent;
--action-button-color: #adadad;
--action-button-hover-color: #37a1ed;
--action-button-active-color: #37a1ed;
--editor-caret-color: #fff;
--editor-selection-background-color: #d7e1f630;

View File

@ -1,4 +1,4 @@
import { sleep } from "../common/async_util.ts";
import { sleep } from "$sb/lib/async.ts";
import type { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import {
SpaceSync,
@ -7,6 +7,7 @@ import {
} from "../common/spaces/sync.ts";
import { EventHook } from "../plugos/hooks/event.ts";
import { KVStore } from "../plugos/lib/kv_store.ts";
import { Space } from "./space.ts";
// Keeps the current sync snapshot
const syncSnapshotKey = "syncSnapshot";
@ -31,11 +32,21 @@ const spaceSyncInterval = 17 * 1000; // Every 17s or so
// Used from Client
export const pageSyncInterval = 6000;
export interface ISyncService {
start(): void;
isSyncing(): Promise<boolean>;
hasInitialSyncCompleted(): Promise<boolean>;
noOngoingSync(_timeout: number): Promise<void>;
syncFile(name: string): Promise<void>;
scheduleFileSync(_path: string): Promise<void>;
scheduleSpaceSync(): Promise<void>;
}
/**
* The SyncService primarily wraps the SpaceSync engine but also coordinates sync between
* different browser tabs. It is using the KVStore to keep track of sync state.
*/
export class SyncService {
export class SyncService implements ISyncService {
spaceSync: SpaceSync;
lastReportedSyncStatus = Date.now();
@ -45,7 +56,6 @@ export class SyncService {
private kvStore: KVStore,
private eventHook: EventHook,
private isSyncCandidate: (path: string) => boolean,
private enabled: boolean,
) {
this.spaceSync = new SpaceSync(
this.localSpacePrimitives,
@ -72,12 +82,15 @@ export class SyncService {
const path = `${name}.md`;
this.scheduleFileSync(path).catch(console.error);
});
this.spaceSync.on({
fileSynced: (meta, direction) => {
eventHook.dispatchEvent("file:synced", meta, direction);
},
});
}
async isSyncing(): Promise<boolean> {
if (!this.enabled) {
return false;
}
const startTime = await this.kvStore.get(syncStartTimeKey);
if (!startTime) {
return false;
@ -95,19 +108,11 @@ export class SyncService {
}
hasInitialSyncCompleted(): Promise<boolean> {
if (!this.enabled) {
return Promise.resolve(true);
}
// Initial sync has happened when sync progress has been reported at least once, but the syncStartTime has been reset (which happens after sync finishes)
return this.kvStore.has(syncInitialFullSyncCompletedKey);
}
async registerSyncStart(fullSync: boolean): Promise<void> {
if (!this.enabled) {
return;
}
// Assumption: this is called after an isSyncing() check
await this.kvStore.batchSet([
{
@ -128,10 +133,6 @@ export class SyncService {
}
async registerSyncProgress(status?: SyncStatus): Promise<void> {
if (!this.enabled) {
return;
}
// Emit a sync event at most every 2s
if (status && this.lastReportedSyncStatus < Date.now() - 2000) {
this.eventHook.dispatchEvent("sync:progress", status);
@ -142,10 +143,6 @@ export class SyncService {
}
async registerSyncStop(isFullSync: boolean): Promise<void> {
if (!this.enabled) {
return;
}
await this.registerSyncProgress();
await this.kvStore.del(syncStartTimeKey);
if (isFullSync) {
@ -162,10 +159,6 @@ export class SyncService {
// Await a moment when the sync is no longer running
async noOngoingSync(timeout: number): Promise<void> {
if (!this.enabled) {
return;
}
// Not completely safe, could have race condition on setting the syncStartTimeKey
const startTime = Date.now();
while (await this.isSyncing()) {
@ -179,10 +172,6 @@ export class SyncService {
filesScheduledForSync = new Set<string>();
async scheduleFileSync(path: string): Promise<void> {
if (!this.enabled) {
return;
}
if (this.filesScheduledForSync.has(path)) {
// Already scheduled, no need to duplicate
console.info(`File ${path} already scheduled for sync`);
@ -195,19 +184,11 @@ export class SyncService {
}
async scheduleSpaceSync(): Promise<void> {
if (!this.enabled) {
return;
}
await this.noOngoingSync(5000);
await this.syncSpace();
}
start() {
if (!this.enabled) {
return;
}
this.syncSpace().catch(console.error);
setInterval(async () => {
@ -227,10 +208,6 @@ export class SyncService {
}
async syncSpace(): Promise<number> {
if (!this.enabled) {
return 0;
}
if (await this.isSyncing()) {
console.log("Aborting space sync: already syncing");
return 0;
@ -258,10 +235,6 @@ export class SyncService {
// Syncs a single file
async syncFile(name: string) {
if (!this.enabled) {
return;
}
// console.log("Checking if we can sync file", name);
if (!this.isSyncCandidate(name)) {
console.info("Requested sync, but not a sync candidate", name);
@ -326,10 +299,6 @@ export class SyncService {
}
await this.saveSnapshot(snapshot);
await this.registerSyncStop(false);
// HEAD
// console.log("And done with file sync for", name);
//
//main
}
async saveSnapshot(snapshot: Map<string, SyncStatusItem>) {
@ -383,3 +352,46 @@ export class SyncService {
return 1;
}
}
/**
* A no-op sync service that doesn't do anything used when running in thin client mode
*/
export class NoSyncSyncService implements ISyncService {
constructor(private space: Space) {
}
isSyncing(): Promise<boolean> {
return Promise.resolve(false);
}
hasInitialSyncCompleted(): Promise<boolean> {
return Promise.resolve(true);
}
noOngoingSync(_timeout: number): Promise<void> {
return Promise.resolve();
}
scheduleFileSync(_path: string): Promise<void> {
return Promise.resolve();
}
scheduleSpaceSync(): Promise<void> {
return Promise.resolve();
}
start() {
setInterval(() => {
// Trigger a page upload for change events
this.space.updatePageList().catch(console.error);
}, spaceSyncInterval);
}
syncSpace(): Promise<number> {
return Promise.resolve(0);
}
syncFile(_name: string): Promise<void> {
return Promise.resolve();
}
}

View File

@ -171,20 +171,9 @@ export function editorSyscalls(editor: Client): SysCallMapping {
return editor.confirm(message);
},
"editor.getUiOption": (_ctx, key: string): any => {
if (key === "thinClientMode") {
return !!localStorage.getItem("thinClientMode");
}
return (editor.ui.viewState.uiOptions as any)[key];
},
"editor.setUiOption": (_ctx, key: string, value: any) => {
if (key === "thinClientMode") {
if (value) {
localStorage.setItem("thinClientMode", "true");
} else {
localStorage.removeItem("thinClientMode");
}
return;
}
editor.ui.viewDispatch({
type: "set-ui-option",
key,

View File

@ -5,8 +5,8 @@ import { CommandDef } from "../hooks/command.ts";
import { proxySyscall } from "./util.ts";
export function systemSyscalls(
editor: Client,
system: System<any>,
client?: Client,
): SysCallMapping {
const api: SysCallMapping = {
"system.invokeFunction": (
@ -38,24 +38,36 @@ export function systemSyscalls(
if (!functionDef) {
throw Error(`Function ${name} not found`);
}
if (functionDef.env && system.env && functionDef.env !== system.env) {
if (
client && functionDef.env && system.env &&
functionDef.env !== system.env
) {
// Proxy to another environment
return proxySyscall(ctx, editor.remoteSpacePrimitives, name, args);
return proxySyscall(ctx, client.remoteSpacePrimitives, name, args);
}
return plug.invoke(name, args);
},
"system.invokeCommand": (_ctx, name: string) => {
return editor.runCommandByName(name);
if (!client) {
throw new Error("Not supported");
}
return client.runCommandByName(name);
},
"system.listCommands": (): { [key: string]: CommandDef } => {
if (!client) {
throw new Error("Not supported");
}
const allCommands: { [key: string]: CommandDef } = {};
for (const [cmd, def] of editor.system.commandHook.editorCommands) {
for (const [cmd, def] of client.system.commandHook.editorCommands) {
allCommands[cmd] = def.command;
}
return allCommands;
},
"system.reloadPlugs": () => {
return editor.loadPlugs();
if (!client) {
throw new Error("Not supported");
}
return client.loadPlugs();
},
"system.getEnv": () => {
return system.env;

View File

@ -4,7 +4,10 @@ release.
---
## Next
* Another heavy behind-the-scenes refactoring release, refactoring the large “core” plug into multiple smaller ones, documentation to be updated to reflect this.
This release brings a new default [[Client Modes|client mode]] to SilverBullet: online mode, which does not sync content to the client but keeps it all at the server. More information: [[Client Modes]].
Other notable changes:
* Massive reshuffling of built-in [[🔌 Plugs]], splitting the old “core” plug into [[🔌 Editor]], [[🔌 Template]] and [[🔌 Index]].
* Removed [[Cloud Links]] support in favor of [[Federation]]
---
@ -63,7 +66,7 @@ release.
* Initial work on [[Attributes]] (inline [[Metadata]]) such as this [importance:: high]
* Added {[Debug: Reset Client]} command that flushes the local databases and caches (and service worker) for debugging purposes.
* Added {[Editor: Center Cursor]} command.
* New template helper `replaceRegexp`, see [[🔌 Core/Templates@vars]]
* New template helper `replaceRegexp`, see [[🔌 Template@vars]]
* **Bug fix**: Renaming of pages now works again on iOS
* Big internal code refactor
@ -80,7 +83,7 @@ release.
## 0.3.4
* **Breaking change (for some templates):** Template in various places allowed you to use `{{variables}}` and various handlebars functions. There also used to be a magic `{{page}}` variable that you could use in various places, but not everywhere. This has now been unified. And the magical `{{page}}` now has been replaced with the global `@page` which does not just expose the pages name, but any page meta data. More information here: [[🔌 Core/Templates@vars]]. You will now get completion for built-in handlebars helpers after typing `{{`.
* **Breaking change (for some templates):** Template in various places allowed you to use `{{variables}}` and various handlebars functions. There also used to be a magic `{{page}}` variable that you could use in various places, but not everywhere. This has now been unified. And the magical `{{page}}` now has been replaced with the global `@page` which does not just expose the pages name, but any page meta data. More information here: [[🔌 Template@vars]]. You will now get completion for built-in handlebars helpers after typing `{{`.
* **Breaking change** (for [[STYLES]] users). The [[STYLES]] page is now no longer “magic” and hardcoded. It can (and must) now be specified in [[SETTINGS]] (see example on that page) for styles to be loaded from it.
* Folding is here (at least with commands, not much UI): {[Fold: Fold]}, {[Fold: Unfold]}, {[Fold: Toggle Fold]}, {[Fold: Fold All]} and {[Fold: Unfold All]}.
* {[Broken Links: Show]} command (not complete yet, but already useful)

30
website/Client Modes.md Normal file
View File

@ -0,0 +1,30 @@
SilverBullet currently supports two modes for its client:
1. _Online mode_ (the default): keeps all content on the server
2. _Synced mode_ (offline capable): syncs all content to the client
You can toggle between these two modes by clicking the 🔄 button in the top bar.
You can switch modes at any time, so try them both to decide what works best for you.
## Online mode
In online mode, all content in your space is kept on the server, and a lot of the heavy lifting (such as indexing of pages) happens on the server.
Advantages:
* **Keeps content on the server**: this mode not synchronize all your content to your client (browser), making this a better fit for large spaces.
* **Lighter-weight** in terms of memory and CPU use of the client
Disadvantages:
* **Requires a working network connection** to the server.
* **Higher latency**, since more interactions require calls to the server, this may be notable e.g. when completing page names.
## Synced mode
In this mode, all content is synchronized to the client, and all processing happens there. The server effectively acts as “dumb data store.” All SilverBullet functionality is available even when there is no network connection available.
Advantages:
* **100% offline capable**: disconnect your client from the network, shutdown the server, everything still works. Changes synchronize automatically once a network connection is re-established.
* **Lower latency**: all actions are performed locally in the client, which in most cases will be faster
Disadvantages:
* **Synchronizes all content onto your client**: using disk space and an initially large bulk of network traffic to download everything.

View File

@ -24,6 +24,6 @@ Metadata is data about data. There are a few entities you can add meta data to:
In addition, this metadata can be augmented in a few additional ways:
* [[🔌 Core/Tags]]: adds to the `tags` attribute
* [[Tags]]: adds to the `tags` attribute
* [[Frontmatter]]: at the top of pages, a [[YAML]] encoded block can be used to define additional attributes to a page
* [[Attributes]]

35
website/PlugOS.md Normal file
View File

@ -0,0 +1,35 @@
So heres a secret — [[SilverBullet]] is really just a trojan horse to test a potentially much more widely applicable idea, the idea to _make applications extensible at different levels of its stack_ in a controlled manner.
## Background
Ive long appreciated the simplicity and flexibility of [AWSs lambda functions](https://aws.amazon.com/lambda/). The idea is simple: you write a function using some language (JavaScript, Python, Java or whatever floats your boat), package it up, and ship it to AWS (think: zip file). Then, you configure the triggers that invoke those functions (such as certain events) and thats it. The rest is managed for you.
The AWS infrastructure fully manages the lifecycle of these functions: it ensures there are sufficient servers ready to invoke them, runs the code, recycles the processes when appropriate, and kills them when they misbehave. All this machinery is completely hidden from the user. It is referred to as **serverless** because it abstracts away the concept of a server.
Of course, this requires functions to be written in a specific way:
* **Stateless:** while the runtime may keep functions running and reuse an instance to perform multiple invocations, functions have to be written without this assumption. Therefore any state needs to be maintained outside of the function.
* **Self contained:** they make limited assumptions on the environment other than a language runtime, typically.
* **Short lived:** the assumption is that functions run for a limited amount of time, usually a few milliseconds, perhaps seconds, but a minute at most.
While they can perform arbitrary computations, they do have constraints:
1. They have to be stateless: while the runtime may keep functions running and reuse an instance to perform multiple invocations, they cannot assume this is the case. They have to assume that every invocation happens in a fresh environment.
2. They have limited access to the host machine, such as no direct access to a (persistent) file system.
What can these functions do? In principle, anything, while being limited to access to the host. They generally cannot write to the hosts filesystem for instance. They also tend to be constrained in allocated run time and memory. All communication with the outside world tends to happen
Then, you configure when it should be triggered.
This concept is not only interesting in terms of **scalability** such a function can quickly scale to millions of invocations per second when necessary, and down to zero when that demand vanishes — but also in terms of **portability**. Couldnt such functions conceptually run _everywhere_? And indeed, recently such functions have been moving to whats called “the edge” as well, such as [Lambda@Edge](https://aws.amazon.com/lambda/edge/), [Vercels Edge Functions](https://vercel.com/blog/edge-functions-generally-available), or [Netlifys Edge Functions](https://docs.netlify.com/edge-functions/overview/). What is the “edge” here? Generally, the closest data center these providers offer near the user. The goal? Lower latency.
But is that is as _edgy_ as we can get? What about the _real_ edge: the users device?
## Introducing PlugOS
PlugOS is a JavaScript (TypeScript) library that brings these concepts to _applications_: allowing applications, [[SilverBullet]] to be extended in a safe way, by allowing plugins — named “plugs” — to _hook_ into various aspects of the application, run custom code as a result, which in turn can affect the application again via _syscalls_.
## Concepts
* _Functions_: are pieces of code, written in JavaScript or TypeScript that add custom functionality to a hosting application.
* _Hooks_: are application-specific extension points, they can range from defining new commands, to timer based hooks (cron-like), to HTTP endpoints to be defined.
* _Syscalls_: expose (often) application-specific functionality to functions, allowing it to e.g. manipulate the UI, access various data stores etc.
* _Manifests_: wire the whole thing together, they are [[YAML]] files that define the functions and what they hook into.
* _Sandbox_: each plug is run in its own sandbox, in the browser this is a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API), on the server as well (although Deno enables [deeper sandboxing](https://deno.land/manual@v1.36.3/runtime/workers#instantiation-permissions) than the browser). Sandboxes can, in principle, be flushed out and restarted at any time. In fact, this is how _hot reloading_ of plugs is implemented.

View File

@ -1,4 +1,4 @@
SilverBullet is an extensible, [open source](https://github.com/silverbulletmd/silverbullet), **personal knowledge management** system. Indeed, thats fancy talk for “a note-taking app with links.” However, SilverBullet goes a bit beyond _just_ that.
SilverBullet is an extensible, [open source](https://github.com/silverbulletmd/silverbullet), **personal knowledge management** system. Indeed, thats fancy talk for “a note-taking app with links.” However, SilverBullet goes _a bit_ beyond just that.
Youve been told there is _no such thing_ as a [silver bullet](https://en.wikipedia.org/wiki/Silver_bullet). You were told wrong.
@ -7,7 +7,7 @@ Before we get to the nitty gritty, some _quick links_ for the impatient reader:
Now that we got that out of the way lets have a look at some of SilverBullets features.
## Features
* Runs in any modern browser (including on mobile) as an **offline-first [[PWA]],** keeping the primary copy of your content in the browser, syncing back to the server when a network connection is available.
* Runs in any modern browser (including on mobile) as a [[PWA]] in two potential [[Client Modes]] (_online_ and _synced_ mode), where the _synced mode_ enables **100% offline operation**, keeping a copy of content in the browser, syncing back to the server when a network connection is available.
* Provides an enjoyable [[Markdown]] writing experience with a clean UI, rendering text using [[Live Preview|live preview]], further **reducing visual noise** while still providing direct access to the underlying markdown syntax.
* Supports wiki-style **page linking** using the `[[page link]]` syntax, even keeping links up-to-date when pages are renamed.
* Optimized for **keyboard-based operation**:

View File

@ -6,7 +6,7 @@ Tags in SilverBullet can be added in two ways:
For instance, by using the #core-tag in this page, it has been tagged and can be used in a [[🔌 Directive/Query]]:
<!-- #query page where tags = "core-tag" render [[template/page]] -->
* [[🔌 Core/Tags]]
* [[Tags]]
<!-- /query -->
Similarly, tags can be applied to list **items**:
@ -16,9 +16,9 @@ Similarly, tags can be applied to list **items**:
and be queried:
<!-- #query item where tags = "core-tag" -->
|name |tags |page |pos|
|-------------------------------|--------|------------|---|
|This is a tagged item #core-tag|core-tag|🔌 Core/Tags|493|
|name |tags |page|pos|
|-------------------------------|--------|----|---|
|This is a tagged item #core-tag|core-tag|Tags|494|
<!-- /query -->
and **tags**:
@ -28,5 +28,5 @@ and **tags**:
And they can be queried this way:
<!-- #query task where tags = "core-tag" render [[template/task]] -->
* [ ] [[🔌 Core/Tags@804]] This is a tagged task #core-tag
* [ ] [[Tags@808]] This is a tagged task #core-tag
<!-- /query -->

View File

@ -1,16 +0,0 @@
---
type: plug
repo: https://github.com/silverbulletmd/silverbullet
---
The core plug implements foundational functionality for SilverBullet. It covers the following areas:
* [[🔌 Core/Indexing]]
* [[🔌 Core/Templates]]
* [[🔌 Core/Tags]]
* [[🔌 Core/Full Text Search]]
* [[🔌 Core/Slash Commands]]
* [[🔌 Core/Edit Commands]]
* [[🔌 Core/Plug Management]]
* [[🔌 Core/Link Unfurl]]

View File

@ -1,4 +1,4 @@
The [[🔌 Core]] plug provides various useful edit commands, such as:
The [[🔌 Editor]] plug provides various useful edit commands, such as:
* {[Text: Bold]} {[Text: Italic]} {[Text: Marker]} to respectively make text bold, italic or mark it.
* {[Text: Listify Selection]} to turn each line in the selection into a (bullet) list

View File

@ -1,7 +0,0 @@
SilverBullet has a generic indexing infrastructure. Pages are reindexed upon saving, so about every second. Manual reindexing can be done running the {[Space: Reindex]} command.
The [[🔌 Core]] plug indexes the following:
* Page metadata encoded in [[Frontmatter]] (queryable via the `page` query source)
* Page backlinks (queryable via the `link` query source), this information is used when renaming a page (automatically updating pages that link to it). Renaming can be done either by editing the page name in the header and hitting `Enter`, or using the {[Page: Rename]} command.
* List items, such as bulleted and numbered lists (queryable via the `item` query source)

View File

@ -1,10 +1,10 @@
Plug management using the [[PLUGS]] file is also implemented in the [[🔌 Core]] plug.
Plug management using the [[PLUGS]] file is also implemented in the [[🔌 Editor]] plug.
The optional [[PLUGS]] file is only processed when running the {[Plugs: Update]} command, in which case it will fetch all the listed plugs and copy them into the (hidden) `_plug/` folder in the users space. SilverBullet loads these files on boot (or on demand after running the {[Plugs: Update]} command).
You can also use the {[Plugs: Add]} to add a plug, which will automatically create a [[PLUGS]] if it does not yet exist.
The [[🔌 Core]] plug has support for the following URI prefixes for plugs:
The [[🔌 Editor]] plug has support for the following URI prefixes for plugs:
* `https:` loading plugs via HTTPS, e.g. `[https://](https://raw.githubusercontent.com/silverbulletmd/silverbullet-github/main/github.plug.json)`
* `github:org/repo/file.plug.json` internally rewritten to a `https` url as above.

View File

@ -1,10 +1,10 @@
Slash commands are built-in to SilverBullet. You can trigger them by typing a `/` in your text (after whitespace).
The [[🔌 Core]] plug provides a few helpful ones:
The [[🔌 Editor]] plug provides a few helpful ones:
* `/h1` through `/h4` to turn the current line into a header
* `/hr` to insert a horizontal rule (`---`)
* `/table` to insert a markdown table (whoever can remember this syntax without it)
* `/snippet` see [[🔌 Core/Templates@snippets]]
* `/snippet` see [[🔌 Template@snippets]]
* `/today` to insert todays date
* `/tomorrow` to insert tomorrows date

View File

@ -47,7 +47,7 @@ So, for instance, a template can take a tag name as an argument:
$eval
The `#eval` directive can be used to evaluate arbitrary JavaScript expressions. Its also possible to invoke arbitrary plug functions this way.
**Note:** This feature is experimental and will likely evolve.
**Note:** ==This feature is experimental== and will likely evolve.
A simple example is multiplying numbers:

View File

@ -51,7 +51,7 @@ The best part about data sources: there is auto-completion. 🎉
Start writing `<!— #query` or simply use `/query` slash command, it will show you all available data sources. 🤯
Additionally there are [[🔌 Core/Templates@vars|special variables]] you can use in your queries.
Additionally there are [[🔌 Template@vars|special variables]] you can use in your queries.
For example, if you wanted a query for all the tasks from a previous day's daily note, you could use the following query:
`<!-- #query task where page = "📅 {{yesterday}}" -->`

50
website/🔌 Editor.md Normal file
View File

@ -0,0 +1,50 @@
---
type: plug
repo: https://github.com/silverbulletmd/silverbullet
---
The `editor` plug implements foundational editor functionality for SilverBullet.
## Commands
* {[Editor: Toggle Dark Mode]}: toggles dark mode
* {[Editor: Toggle Vim Mode]}: toggle vim mode, see: [[Vim]]
* {[Stats: Show]}: shows some stats about the current page (word count, reading time etc.)
* {[Help: Getting Started]}: Open getting started guide
* {[Help: Version]}: Show version number
### Pages
* {[Page: New]}: Create a new (untitled) page. Note that usually you would create a new page simply by navigating to a page name that does not yet exist.
* {[Page: Delete]}: delete the current page
* {[Page: Copy]}: copy the current page
### Navigation
* {[Navigate: Home]}: navigate to the home (index) page
* {[Navigate To page]}: navigate to the page under the cursor
* {[Navigate: Center Cursor]}: center the cursor at the center of the screen
* {[Navigate: Move Cursor to Position]}: move cursor to a specific (numeric) cursor position (# of characters from the start of the document)
### Text editing
* {[Text: Quote Selection]}: turns the selection into a blockquote (`>` prefix)
* {[Text: Listify Selection]}: turns the lines in the selection into a bulleted list
* {[Text: Number Listify Selection]}: turns the lines in the selection into a numbered list
* {[Text: Link Selection]}: turns the selection into a link.
#ProTip You can can also select text and paste a URL on it via `Ctrl-v`/`Cmd-v` to turn it into a link)
* {[Text: Bold]}: make text **bold**
* {[Text: Italic]}: make text _italic_
* {[Text: Marker]}: mark text with a ==marker color==
* {[Link: Unfurl]}: “Unfurl” a link, see [[🔌 Editor/Link Unfurl]]
### Folding commands
* {[Fold: Fold]}: fold current section (list, header)
* {[Fold: Unfold]}: unfold current section
* {[Fold: Fold All]}: fold all sections
* {[Fold: Unfold All]}: unfold all sections
## Debug
Commands you shouldnt need, but are nevertheless there:
* {[Debug: Reset Client]}: clean out all cached data on the client and reload
* {[Debug: Reload UI]}: reload the UI (same as refreshing the page)
* {[Account: Logout]}: (when using built-in [[Authentication]]) Logout

View File

@ -2,4 +2,4 @@ SilverBullet has infrastructure to “unfurl” — that is: replace with somet
Plugs can provide custom unfurls for specific URL patterns. For instance the [[🔌 Twitter]] plug provides the ability to unfurl tweets, and pull in their content.
[[🔌 Core]] provides a generic URL unfurl, adding a title for a url.
[[🔌 Editor]] provides a generic URL unfurl, adding a title for a url.

22
website/🔌 Index.md Normal file
View File

@ -0,0 +1,22 @@
---
type: plug
repo: https://github.com/silverbulletmd/silverbullet
---
SilverBullet has a generic indexing infrastructure. Pages are reindexed upon saving, so about every second.
The [[🔌 Index]] plug also defines syntax for [[Tags]]
## Content indexing
The [[🔌 Index]] plug indexes the following:
* [[Metadata]]
* [[Tags]]
* Page backlinks (queryable via the `link` query source), this information is used when renaming a page (automatically updating pages that link to it).
* List items, such as bulleted and numbered lists (queryable via the `item` query source)
## Commands
* {[Space: Reindex]}: reindex the entire
* {[Page: Rename]}: Rename a page
#ProTip Renaming is more conveniently done by editing the page name in the header and hitting `Enter`.
* {[Page: Batch Rename Prefix]}: Rename a page prefix across the entire space
* {[Page: Extract]}: Extract the selected text into its own page

View File

@ -1,6 +1,6 @@
SilverBullet at its core is bare bones in terms of functionality, most of its power it gains from **plugs**.
Plugs are an extension mechanism (implemented using a library called PlugOS thats part of the silverbullet repo) that runs “plug” code in the browser using [web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers).
Plugs are an extension mechanism (implemented using a library called [[PlugOS]] thats part of the silverbullet repo) that runs “plug” code in the browser using [web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers).
Plugs can hook into SB in various ways:
@ -17,12 +17,14 @@ Plugs are distributed as self-contained JavaScript bundles (ending with `.plug.j
## Core plugs
These plugs are distributed with SilverBullet and are automatically enabled:
<!-- #query page where type = "plug" and uri = null order by name render [[template/plug]] -->
* [[🔌 Core]]
* [[🔌 Directive]]
* [[🔌 Editor]]
* [[🔌 Emoji]]
* [[🔌 Index]]
* [[🔌 Markdown]]
* [[🔌 Share]]
* [[🔌 Tasks]]
* [[🔌 Tasks]]
* [[🔌 Template]]
<!-- /query -->
## Third-party plugs
@ -85,7 +87,6 @@ Within seconds (watch your browsers JavaScript console), your plug should be
Since plugs run in your browser, you can use the usual browser debugging tools. When you console.log things, these logs will appear in your browsers JavaScript console.
## Distribution
Once youre happy with your plug, you can distribute it in various ways:
- You can put it on github by simply committing the resulting `.plug.js` file there and instructing users to point to by adding

View File

@ -9,7 +9,7 @@ Tasks in SilverBullet are written using semi-standard task syntax:
* [ ] This is a task
Tasks can also be annotated with [[🔌 Core/Tags]]:
Tasks can also be annotated with [[Tags]]:
* [ ] This is a tagged task #my-tag
@ -27,6 +27,6 @@ This metadata is extracted and available via the `task` query source to [[🔌 D
|name |done |page |pos|tags |deadline |
|-----------------------------|-----|--------|---|------|----------|
|This is a task |false|🔌 Tasks|213| | |
|This is a tagged task #my-tag|false|🔌 Tasks|287|my-tag| |
|This is due |false|🔌 Tasks|573| |2022-11-26|
|This is a tagged task #my-tag|false|🔌 Tasks|279|my-tag| |
|This is due |false|🔌 Tasks|565| |2022-11-26|
<!-- /query -->

View File

@ -1,7 +1,11 @@
The core plug implements a few templating mechanisms.
---
type: plug
repo: https://github.com/silverbulletmd/silverbullet
---
The [[🔌 Template]] plug implements a few templating mechanisms.
### Page Templates
The {[Template: Instantiate Page]} command enables you to create a new page based on a page template.
Page templates, by default, are looked for in the `template/page/` prefix. So creating e.g. a `template/page/Meeting Notes` page will create a “Meeting Notes” template. You can override this prefix by setting the `pageTemplatePrefix` in `SETTINGS`.
@ -64,6 +68,16 @@ with a 🗓️ emoji by default, but this is configurable via the `weeklyNotePre
The {[Quick Note]} command will navigate to an empty page named with the current date and time prefixed with a 📥 emoji, but this is configurable via the `quickNotePrefix` in `SETTINGS`. The use case is to take a quick note outside of your current context.
## Slash commands
* `/front-matter`: Insert [[Frontmatter]]
* `/h1` - `/h4`: turn the current line into a header
* `/code`: insert a fenced code block
* `/hr`: insert a horizontal rule
* `/table`: insert a table
* `/page-template`: insert a page template
* `/today`: insert todays date
* `/tomorrow`: insert tomorrows date
### Template helpers
$vars
Currently supported (hardcoded in the code):