Work on client modes
This commit is contained in:
parent
5ff1a8bae3
commit
9a005f26b5
@ -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";
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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));
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
26
plug-api/lib/async.test.ts
Normal file
26
plug-api/lib/async.test.ts
Normal 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
69
plug-api/lib/async.ts
Normal 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
|
||||
}
|
||||
}
|
20
plug-api/lib/async_util.test.ts
Normal file
20
plug-api/lib/async_util.test.ts
Normal 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);
|
||||
});
|
@ -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 () => {
|
||||
|
@ -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]);
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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();
|
||||
}
|
@ -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
|
||||
|
@ -96,8 +96,6 @@ functions:
|
||||
events:
|
||||
- editor:complete
|
||||
|
||||
|
||||
|
||||
# Hashtags
|
||||
indexTags:
|
||||
path: "./tags.ts:indexTags"
|
||||
|
@ -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),
|
||||
|
@ -2,8 +2,6 @@ name: search
|
||||
functions:
|
||||
indexPage:
|
||||
path: search.ts:indexPage
|
||||
# Only enable in client for now
|
||||
# env: client
|
||||
events:
|
||||
- page:index
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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(
|
||||
|
@ -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(),
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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)`,
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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 page’s 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 page’s 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
30
website/Client Modes.md
Normal 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.
|
||||
|
@ -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
35
website/PlugOS.md
Normal file
@ -0,0 +1,35 @@
|
||||
So here’s 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
|
||||
I’ve long appreciated the simplicity and flexibility of [AWS’s 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 that’s 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 host’s 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**. Couldn’t such functions conceptually run _everywhere_? And indeed, recently such functions have been moving to what’s called “the edge” as well, such as [Lambda@Edge](https://aws.amazon.com/lambda/edge/), [Vercel’s Edge Functions](https://vercel.com/blog/edge-functions-generally-available), or [Netlify’s 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 user’s 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.
|
@ -1,4 +1,4 @@
|
||||
SilverBullet is an extensible, [open source](https://github.com/silverbulletmd/silverbullet), **personal knowledge management** system. Indeed, that’s 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, that’s fancy talk for “a note-taking app with links.” However, SilverBullet goes _a bit_ beyond just that.
|
||||
|
||||
You’ve 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 let’s have a look at some of SilverBullet’s 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**:
|
||||
|
@ -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 -->
|
@ -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]]
|
||||
|
@ -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
|
||||
|
@ -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)
|
@ -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 user’s 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.
|
||||
|
@ -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 today’s date
|
||||
* `/tomorrow` to insert tomorrow’s date
|
||||
|
@ -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. It’s 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:
|
||||
|
||||
|
@ -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
50
website/🔌 Editor.md
Normal 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 shouldn’t 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
|
||||
|
@ -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
22
website/🔌 Index.md
Normal 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
|
@ -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 that’s 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]] that’s 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 browser’s 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 browser’s JavaScript console.
|
||||
|
||||
## Distribution
|
||||
|
||||
Once you’re 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
|
||||
|
@ -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 -->
|
||||
|
@ -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 today’s date
|
||||
* `/tomorrow`: insert tomorrow’s date
|
||||
|
||||
### Template helpers
|
||||
$vars
|
||||
Currently supported (hardcoded in the code):
|
Loading…
Reference in New Issue
Block a user