More event refactoring work

pull/513/head
Zef Hemel 2023-08-27 14:13:18 +02:00
parent 593597454a
commit c3d384330d
13 changed files with 140 additions and 149 deletions

View File

@ -5,64 +5,59 @@ import type { SpacePrimitives } from "./space_primitives.ts";
/** /**
* Events exposed: * Events exposed:
* - file:changed (FileMeta) * - file:changed (string, localUpdate: boolean)
* - file:deleted (string) * - file:deleted (string)
* - file:listed (FileMeta[]) * - file:listed (FileMeta[])
* - page:saved (string, FileMeta) * - page:saved (string, FileMeta)
* - page:deleted (string) * - page:deleted (string)
*/ */
export class EventedSpacePrimitives implements SpacePrimitives { export class EventedSpacePrimitives implements SpacePrimitives {
private fileMetaCache = new Map<string, FileMeta>(); alreadyFetching = false;
initialFileListLoad = true; initialFileListLoad = true;
spaceSnapshot: Record<string, number> = {};
constructor( constructor(
private wrapped: SpacePrimitives, private wrapped: SpacePrimitives,
private eventHook: EventHook, private eventHook: EventHook,
private eventsToDispatch = [
"file:changed",
"file:deleted",
"file:listed",
"page:saved",
"page:deleted",
],
) {} ) {}
dispatchEvent(name: string, ...args: any[]): Promise<any[]> { dispatchEvent(name: string, ...args: any[]): Promise<any[]> {
if (this.eventsToDispatch.includes(name)) {
return this.eventHook.dispatchEvent(name, ...args); return this.eventHook.dispatchEvent(name, ...args);
} else {
return Promise.resolve([]);
}
} }
async fetchFileList(): Promise<FileMeta[]> { async fetchFileList(): Promise<FileMeta[]> {
const newFileList = await this.wrapped.fetchFileList(); const newFileList = await this.wrapped.fetchFileList();
const deletedFiles = new Set<string>(this.fileMetaCache.keys()); if (this.alreadyFetching) {
// Avoid race conditions
return newFileList;
}
// console.log("HEREEREEEREEREE");
this.alreadyFetching = true;
const deletedFiles = new Set<string>(Object.keys(this.spaceSnapshot));
for (const meta of newFileList) { for (const meta of newFileList) {
const oldFileMeta = this.fileMetaCache.get(meta.name); const oldHash = this.spaceSnapshot[meta.name];
const newFileMeta: FileMeta = { ...meta }; const newHash = meta.lastModified;
if ( if (
( (
// New file scenario // New file scenario
!oldFileMeta && !this.initialFileListLoad !oldHash && !this.initialFileListLoad
) || ( ) || (
// Changed file scenario // Changed file scenario
oldFileMeta && oldHash &&
oldFileMeta.lastModified !== newFileMeta.lastModified oldHash !== newHash
) )
) { ) {
this.dispatchEvent("file:changed", newFileMeta); this.dispatchEvent("file:changed", meta.name);
} }
// Page found, not deleted // Page found, not deleted
deletedFiles.delete(meta.name); deletedFiles.delete(meta.name);
// Update in cache // Update in snapshot
this.fileMetaCache.set(meta.name, newFileMeta); this.spaceSnapshot[meta.name] = newHash;
} }
for (const deletedFile of deletedFiles) { for (const deletedFile of deletedFiles) {
this.fileMetaCache.delete(deletedFile); delete this.spaceSnapshot[deletedFile];
this.dispatchEvent("file:deleted", deletedFile); this.dispatchEvent("file:deleted", deletedFile);
if (deletedFile.endsWith(".md")) { if (deletedFile.endsWith(".md")) {
@ -71,28 +66,18 @@ export class EventedSpacePrimitives implements SpacePrimitives {
} }
} }
const fileList = [...new Set(this.fileMetaCache.values())]; this.dispatchEvent("file:listed", newFileList);
this.dispatchEvent("file:listed", fileList); this.alreadyFetching = false;
this.initialFileListLoad = false; this.initialFileListLoad = false;
return fileList; return newFileList;
} }
async readFile( async readFile(
name: string, name: string,
): Promise<{ data: Uint8Array; meta: FileMeta }> { ): Promise<{ data: Uint8Array; meta: FileMeta }> {
const data = await this.wrapped.readFile(name); const data = await this.wrapped.readFile(name);
const previousMeta = this.fileMetaCache.get(name); this.triggerEventsAndCache(name, data.meta.lastModified);
const newMeta = data.meta; return data;
if (previousMeta) {
if (previousMeta.lastModified !== newMeta.lastModified) {
// Page changed since last cached metadata, trigger event
this.dispatchEvent("file:changed", newMeta);
}
}
return {
data: data.data,
meta: this.metaCacher(name, newMeta),
};
} }
async writeFile( async writeFile(
@ -108,9 +93,9 @@ export class EventedSpacePrimitives implements SpacePrimitives {
meta, meta,
); );
if (!selfUpdate) { if (!selfUpdate) {
this.dispatchEvent("file:changed", newMeta); this.dispatchEvent("file:changed", name, true);
} }
this.metaCacher(name, newMeta); this.spaceSnapshot[name] = newMeta.lastModified;
// This can happen async // This can happen async
if (name.endsWith(".md")) { if (name.endsWith(".md")) {
@ -136,17 +121,21 @@ export class EventedSpacePrimitives implements SpacePrimitives {
return newMeta; return newMeta;
} }
triggerEventsAndCache(name: string, newHash: number) {
const oldHash = this.spaceSnapshot[name];
if (oldHash && oldHash !== newHash) {
// Page changed since last cached metadata, trigger event
this.dispatchEvent("file:changed", name);
}
this.spaceSnapshot[name] = newHash;
return;
}
async getFileMeta(name: string): Promise<FileMeta> { async getFileMeta(name: string): Promise<FileMeta> {
try { try {
const oldMeta = this.fileMetaCache.get(name);
const newMeta = await this.wrapped.getFileMeta(name); const newMeta = await this.wrapped.getFileMeta(name);
if (oldMeta) { this.triggerEventsAndCache(name, newMeta.lastModified);
if (oldMeta.lastModified !== newMeta.lastModified) { return newMeta;
// Changed on disk, trigger event
this.dispatchEvent("file:changed", newMeta);
}
}
return this.metaCacher(name, newMeta);
} catch (e: any) { } catch (e: any) {
console.log("Checking error", e, name); console.log("Checking error", e, name);
if (e.message === "Not found") { if (e.message === "Not found") {
@ -167,15 +156,7 @@ export class EventedSpacePrimitives implements SpacePrimitives {
} }
// await this.getPageMeta(name); // Check if page exists, if not throws Error // await this.getPageMeta(name); // Check if page exists, if not throws Error
await this.wrapped.deleteFile(name); await this.wrapped.deleteFile(name);
this.fileMetaCache.delete(name); delete this.spaceSnapshot[name];
this.dispatchEvent("file:deleted", name); this.dispatchEvent("file:deleted", name);
} }
private metaCacher(name: string, meta: FileMeta): FileMeta {
if (meta.lastModified !== 0) {
// Don't cache metadata for pages with a 0 lastModified timestamp (usualy dynamically generated pages)
this.fileMetaCache.set(name, meta);
}
return meta;
}
} }

View File

@ -1,6 +1,14 @@
import type { CommandDef } from "../../web/hooks/command.ts"; import type { CommandDef } from "../../web/hooks/command.ts";
import { syscall } from "./syscall.ts"; import { syscall } from "./syscall.ts";
export function invoke(
name: string,
...args: any[]
): Promise<any> {
return syscall("system.invoke", name, ...args);
}
// @deprecated use invoke instead
export function invokeFunction( export function invokeFunction(
env: string, env: string,
name: string, name: string,

View File

@ -28,9 +28,7 @@ export interface Manifest<HookT> {
* *
* see: common/manifest.ts#SilverBulletHooks * see: common/manifest.ts#SilverBulletHooks
*/ */
functions: { functions: Record<string, FunctionDef<HookT>>;
[key: string]: FunctionDef<HookT>;
};
} }
/** Associates hooks with a function. This is the generic base structure, that identifies the function. Hooks are defined by the type parameter. */ /** Associates hooks with a function. This is the generic base structure, that identifies the function. Hooks are defined by the type parameter. */

View File

@ -37,6 +37,7 @@ functions:
name: "Editor: Move Cursor to Position" name: "Editor: Move Cursor to Position"
clearPageIndex: clearPageIndex:
path: "./page.ts:clearPageIndex" path: "./page.ts:clearPageIndex"
env: server
events: events:
- page:saved - page:saved
- page:deleted - page:deleted
@ -46,6 +47,7 @@ functions:
- query:page - query:page
parseIndexTextRepublish: parseIndexTextRepublish:
path: "./page.ts:parseIndexTextRepublish" path: "./page.ts:parseIndexTextRepublish"
env: server
events: events:
- page:index_text - page:index_text
reindexSpaceCommand: reindexSpaceCommand:

View File

@ -13,7 +13,7 @@ import {
import { events, mq } from "$sb/plugos-syscall/mod.ts"; import { events, mq } from "$sb/plugos-syscall/mod.ts";
import { applyQuery } from "$sb/lib/query.ts"; import { applyQuery } from "$sb/lib/query.ts";
import { invokeFunction } from "$sb/silverbullet-syscall/system.ts"; import { invoke } from "$sb/silverbullet-syscall/system.ts";
import type { Message } from "$sb/types.ts"; import type { Message } from "$sb/types.ts";
import { sleep } from "../../common/async_util.ts"; import { sleep } from "../../common/async_util.ts";
import { cacheFileListing } from "../federation/federation.ts"; import { cacheFileListing } from "../federation/federation.ts";
@ -135,7 +135,7 @@ export async function reindexSpace() {
console.log("Clearing page index..."); console.log("Clearing page index...");
await index.clearPageIndex(); await index.clearPageIndex();
// Executed this way to not have to embed the search plug code here // Executed this way to not have to embed the search plug code here
await invokeFunction("client", "search.clearIndex"); await invoke("search.clearIndex");
const pages = await space.listPages(); const pages = await space.listPages();
// Queue all page names to be indexed // Queue all page names to be indexed
@ -172,7 +172,7 @@ export async function clearPageIndex(page: string) {
} }
export async function parseIndexTextRepublish({ name, text }: IndexEvent) { export async function parseIndexTextRepublish({ name, text }: IndexEvent) {
// console.log("Reindexing", name); console.log("Reindexing", name);
await events.dispatchEvent("page:index", { await events.dispatchEvent("page:index", {
name, name,
tree: await markdown.parseMarkdown(text), tree: await markdown.parseMarkdown(text),

View File

@ -44,7 +44,7 @@ export async function evalDirectiveRenderer(
const result = await (0, eval)( const result = await (0, eval)(
`(async () => { `(async () => {
function invokeFunction(name, ...args) { function invokeFunction(name, ...args) {
return syscall("system.invokeFunction", "server", name, ...args); return syscall("system.invoke", name, ...args);
} }
return ${replaceTemplateVars(translateJs(expression), pageMeta)}; return ${replaceTemplateVars(translateJs(expression), pageMeta)};
})()`, })()`,

View File

@ -12,6 +12,7 @@ functions:
searchUnindex: searchUnindex:
path: "./search.ts:pageUnindex" path: "./search.ts:pageUnindex"
env: client
events: events:
- page:deleted - page:deleted
searchQueryProvider: searchQueryProvider:

View File

@ -30,10 +30,12 @@ import { shellSyscalls } from "../plugos/syscalls/shell.deno.ts";
import { IDBKeyRange, indexedDB } from "https://esm.sh/fake-indexeddb@4.0.2"; import { IDBKeyRange, indexedDB } from "https://esm.sh/fake-indexeddb@4.0.2";
import { SpacePrimitives } from "../common/spaces/space_primitives.ts"; import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
const fileListInterval = 30 * 1000; // 30s
export class ServerSystem { export class ServerSystem {
system: System<SilverBulletHooks> = new System("server"); system: System<SilverBulletHooks> = new System("server");
spacePrimitives!: SpacePrimitives; spacePrimitives!: SpacePrimitives;
requeueInterval?: number; private requeueInterval?: number;
kvStore?: DenoKVStore; kvStore?: DenoKVStore;
constructor( constructor(
@ -114,19 +116,31 @@ export class ServerSystem {
await this.loadPlugs(); await this.loadPlugs();
// for (let plugPath of await space.listPlugs()) {
// plugPath = path.resolve(this.spacePath, plugPath);
// await this.system.load(
// new URL(`file://${plugPath}`),
// createSandbox,
// );
// }
// Load markdown syscalls based on all new syntax (if any) // Load markdown syscalls based on all new syntax (if any)
this.system.registerSyscalls( this.system.registerSyscalls(
[], [],
markdownSyscalls(buildMarkdown(loadMarkdownExtensions(this.system))), markdownSyscalls(buildMarkdown(loadMarkdownExtensions(this.system))),
); );
setInterval(() => {
space.updatePageList().catch(console.error);
}, fileListInterval);
eventHook.addLocalListener("file:changed", (path, localChange) => {
(async () => {
// console.log("!!!!! FILE CHANGED", path, localChange);
if (!localChange && path.endsWith(".md")) {
const pageName = path.slice(0, -3);
const data = await this.spacePrimitives.readFile(path);
console.log("Outside page change: reindexing", pageName);
// Change made outside of editor, trigger reindex
await eventHook.dispatchEvent("page:index_text", {
name: pageName,
text: new TextDecoder().decode(data.data),
});
}
})().catch(console.error);
});
} }
async loadPlugs() { async loadPlugs() {

View File

@ -321,12 +321,6 @@ export class Client {
} }
})().catch(console.error); })().catch(console.error);
} }
// this.eventHook.addLocalListener("page:deleted", (pageName) => {
// if (pageName === this.currentPage) {
// this.flashNotification("Page does exist, creating as a new page");
// }
// });
} }
initSpace(): SpacePrimitives { initSpace(): SpacePrimitives {
@ -375,20 +369,15 @@ export class Client {
localSpacePrimitives = new EventedSpacePrimitives( localSpacePrimitives = new EventedSpacePrimitives(
this.plugSpaceRemotePrimitives, this.plugSpaceRemotePrimitives,
this.eventHook, this.eventHook,
[
"file:changed",
"file:listed",
"page:deleted",
],
); );
} }
this.space = new Space(localSpacePrimitives, this.kvStore, this.eventHook); this.space = new Space(localSpacePrimitives, this.kvStore, this.eventHook);
this.eventHook.addLocalListener("file:changed", (fileMeta: FileMeta) => { this.eventHook.addLocalListener("file:changed", (path: string) => {
// Only reload when watching the current page (to avoid reloading when switching pages) // Only reload when watching the current page (to avoid reloading when switching pages)
if ( if (
this.space.watchInterval && `${this.currentPage}.md` === fileMeta.name this.space.watchInterval && `${this.currentPage}.md` === path
) { ) {
console.log("Page changed elsewhere, reloading"); console.log("Page changed elsewhere, reloading");
this.flashNotification("Page changed elsewhere, reloading"); this.flashNotification("Page changed elsewhere, reloading");

View File

@ -37,7 +37,6 @@ import { indexProxySyscalls } from "./syscalls/index.proxy.ts";
import { storeProxySyscalls } from "./syscalls/store.proxy.ts"; import { storeProxySyscalls } from "./syscalls/store.proxy.ts";
export class ClientSystem { export class ClientSystem {
system: System<SilverBulletHooks> = new System("client");
commandHook: CommandHook; commandHook: CommandHook;
slashCommandHook: SlashCommandHook; slashCommandHook: SlashCommandHook;
namespaceHook: PlugNamespaceHook; namespaceHook: PlugNamespaceHook;
@ -45,15 +44,19 @@ export class ClientSystem {
codeWidgetHook: CodeWidgetHook; codeWidgetHook: CodeWidgetHook;
plugsUpdated = false; plugsUpdated = false;
mdExtensions: MDExt[] = []; mdExtensions: MDExt[] = [];
system: System<SilverBulletHooks>;
constructor( constructor(
private client: Client, private client: Client,
private kvStore: DexieKVStore, private kvStore: DexieKVStore,
private mq: DexieMQ, private mq: DexieMQ,
private dbPrefix: string, dbPrefix: string,
private eventHook: EventHook, private eventHook: EventHook,
private thinClientMode: boolean, 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.addHook(this.eventHook); this.system.addHook(this.eventHook);
// Plug page namespace hook // Plug page namespace hook

View File

@ -65,36 +65,6 @@ export class Space {
public async updatePageList() { public async updatePageList() {
// This will trigger appropriate events automatically // This will trigger appropriate events automatically
await this.fetchPageList(); await this.fetchPageList();
// const deletedPages = new Set<string>(this.pageMetaCache.keys());
// newPageList.forEach((meta) => {
// const pageName = meta.name;
// const oldPageMeta = this.pageMetaCache.get(pageName);
// const newPageMeta: PageMeta = { ...meta };
// if (
// !oldPageMeta &&
// (pageName.startsWith(plugPrefix) || !this.initialPageListLoad)
// ) {
// this.emit("pageCreated", newPageMeta);
// } else if (
// oldPageMeta &&
// oldPageMeta.lastModified !== newPageMeta.lastModified
// ) {
// this.emit("pageChanged", newPageMeta);
// }
// // Page found, not deleted
// deletedPages.delete(pageName);
// // Update in cache
// this.pageMetaCache.set(pageName, newPageMeta);
// });
// for (const deletedPage of deletedPages) {
// this.pageMetaCache.delete(deletedPage);
// this.emit("pageDeleted", deletedPage);
// }
// this.emit("pageListUpdated", this.listPages());
// this.initialPageListLoad = false;
} }
async deletePage(name: string): Promise<void> { async deletePage(name: string): Promise<void> {

View File

@ -2,17 +2,27 @@ import type { Plug } from "../../plugos/plug.ts";
import { SysCallMapping, System } from "../../plugos/system.ts"; import { SysCallMapping, System } from "../../plugos/system.ts";
import type { Client } from "../client.ts"; import type { Client } from "../client.ts";
import { CommandDef } from "../hooks/command.ts"; import { CommandDef } from "../hooks/command.ts";
import { proxySyscall } from "./util.ts";
export function systemSyscalls( export function systemSyscalls(
editor: Client, editor: Client,
system: System<any>, system: System<any>,
): SysCallMapping { ): SysCallMapping {
return { const api: SysCallMapping = {
"system.invokeFunction": ( "system.invokeFunction": (
ctx, ctx,
_env: string, _env: string,
name: string, name: string,
...args: any[] ...args: any[]
) => {
// For backwards compatibility
// TODO: Remove at some point
return api["system.invoke"](ctx, name, ...args);
},
"system.invoke": (
ctx,
name: string,
...args: any[]
) => { ) => {
if (!ctx.plug) { if (!ctx.plug) {
throw Error("No plug associated with context"); throw Error("No plug associated with context");
@ -28,6 +38,14 @@ export function systemSyscalls(
} }
name = functionName; name = functionName;
} }
const functionDef = plug.manifest!.functions[name];
if (!functionDef) {
throw Error(`Function ${name} not found`);
}
if (functionDef.env && system.env && functionDef.env !== system.env) {
// Proxy to another environment
return proxySyscall(editor.remoteSpacePrimitives, name, args);
}
return plug.invoke(name, args); return plug.invoke(name, args);
}, },
"system.invokeCommand": (_ctx, name: string) => { "system.invokeCommand": (_ctx, name: string) => {
@ -47,4 +65,5 @@ export function systemSyscalls(
return system.env; return system.env;
}, },
}; };
return api;
} }

View File

@ -1,3 +1,4 @@
import { HttpSpacePrimitives } from "../../common/spaces/http_space_primitives.ts";
import { SysCallMapping } from "../../plugos/system.ts"; import { SysCallMapping } from "../../plugos/system.ts";
import { SyscallResponse } from "../../server/rpc.ts"; import { SyscallResponse } from "../../server/rpc.ts";
import { Client } from "../client.ts"; import { Client } from "../client.ts";
@ -5,12 +6,20 @@ import { Client } from "../client.ts";
export function proxySyscalls(client: Client, names: string[]): SysCallMapping { export function proxySyscalls(client: Client, names: string[]): SysCallMapping {
const syscalls: SysCallMapping = {}; const syscalls: SysCallMapping = {};
for (const name of names) { for (const name of names) {
syscalls[name] = async (_ctx, ...args: any[]) => { syscalls[name] = (_ctx, ...args: any[]) => {
if (!client.remoteSpacePrimitives) { return proxySyscall(client.remoteSpacePrimitives, name, args);
throw new Error("Not supported"); };
} }
const resp = await client.remoteSpacePrimitives.authenticatedFetch( return syscalls;
`${client.remoteSpacePrimitives.url}/.rpc`, }
export async function proxySyscall(
httpSpacePrimitives: HttpSpacePrimitives,
name: string,
args: any[],
): Promise<any> {
const resp = await httpSpacePrimitives.authenticatedFetch(
`${httpSpacePrimitives.url}/.rpc`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@ -27,7 +36,4 @@ export function proxySyscalls(client: Client, names: string[]): SysCallMapping {
} else { } else {
return result.result; return result.result;
} }
};
}
return syscalls;
} }