2022-10-10 12:50:21 +00:00
|
|
|
import { FileData, FileEncoding, SpacePrimitives } from "./space_primitives.ts";
|
|
|
|
import { AttachmentMeta, FileMeta, PageMeta } from "../types.ts";
|
|
|
|
import { EventEmitter } from "../../plugos/event.ts";
|
|
|
|
import { Plug } from "../../plugos/plug.ts";
|
|
|
|
import { plugPrefix } from "./constants.ts";
|
|
|
|
import { safeRun } from "../util.ts";
|
2022-04-07 13:21:30 +00:00
|
|
|
|
|
|
|
const pageWatchInterval = 2000;
|
2022-04-05 15:02:17 +00:00
|
|
|
|
|
|
|
export type SpaceEvents = {
|
|
|
|
pageCreated: (meta: PageMeta) => void;
|
|
|
|
pageChanged: (meta: PageMeta) => void;
|
|
|
|
pageDeleted: (name: string) => void;
|
|
|
|
pageListUpdated: (pages: Set<PageMeta>) => void;
|
|
|
|
};
|
|
|
|
|
2022-09-12 12:50:37 +00:00
|
|
|
export class Space extends EventEmitter<SpaceEvents> {
|
2022-04-07 13:21:30 +00:00
|
|
|
pageMetaCache = new Map<string, PageMeta>();
|
|
|
|
watchedPages = new Set<string>();
|
|
|
|
private initialPageListLoad = true;
|
|
|
|
private saving = false;
|
|
|
|
|
2022-09-12 12:50:37 +00:00
|
|
|
constructor(private space: SpacePrimitives) {
|
2022-04-07 13:21:30 +00:00
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
2022-04-26 17:04:36 +00:00
|
|
|
public async updatePageList() {
|
2022-10-15 17:02:56 +00:00
|
|
|
const newPageList = await this.fetchPageList();
|
|
|
|
const deletedPages = new Set<string>(this.pageMetaCache.keys());
|
2022-09-12 12:50:37 +00:00
|
|
|
newPageList.forEach((meta) => {
|
2022-04-26 17:04:36 +00:00
|
|
|
const pageName = meta.name;
|
|
|
|
const oldPageMeta = this.pageMetaCache.get(pageName);
|
2022-11-20 09:24:42 +00:00
|
|
|
const newPageMeta: PageMeta = { ...meta };
|
2022-04-26 17:04:36 +00:00
|
|
|
if (
|
|
|
|
!oldPageMeta &&
|
|
|
|
(pageName.startsWith(plugPrefix) || !this.initialPageListLoad)
|
|
|
|
) {
|
|
|
|
this.emit("pageCreated", newPageMeta);
|
|
|
|
} else if (
|
|
|
|
oldPageMeta &&
|
2022-09-12 12:50:37 +00:00
|
|
|
oldPageMeta.lastModified !== newPageMeta.lastModified
|
2022-04-26 17:04:36 +00:00
|
|
|
) {
|
|
|
|
this.emit("pageChanged", newPageMeta);
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
2022-04-26 17:04:36 +00:00
|
|
|
// Page found, not deleted
|
|
|
|
deletedPages.delete(pageName);
|
2022-04-07 13:21:30 +00:00
|
|
|
|
2022-04-26 17:04:36 +00:00
|
|
|
// Update in cache
|
|
|
|
this.pageMetaCache.set(pageName, newPageMeta);
|
2022-04-07 13:21:30 +00:00
|
|
|
});
|
2022-04-26 17:04:36 +00:00
|
|
|
|
|
|
|
for (const deletedPage of deletedPages) {
|
|
|
|
this.pageMetaCache.delete(deletedPage);
|
|
|
|
this.emit("pageDeleted", deletedPage);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.emit("pageListUpdated", this.listPages());
|
|
|
|
this.initialPageListLoad = false;
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
watch() {
|
|
|
|
setInterval(() => {
|
|
|
|
safeRun(async () => {
|
|
|
|
if (this.saving) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (const pageName of this.watchedPages) {
|
|
|
|
const oldMeta = this.pageMetaCache.get(pageName);
|
|
|
|
if (!oldMeta) {
|
|
|
|
// No longer in cache, meaning probably deleted let's unwatch
|
|
|
|
this.watchedPages.delete(pageName);
|
|
|
|
continue;
|
|
|
|
}
|
2022-04-08 15:46:09 +00:00
|
|
|
// This seems weird, but simply fetching it will compare to local cache and trigger an event if necessary
|
|
|
|
await this.getPageMeta(pageName);
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}, pageWatchInterval);
|
2022-04-26 17:04:36 +00:00
|
|
|
this.updatePageList().catch(console.error);
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
|
|
|
|
2022-10-15 17:02:56 +00:00
|
|
|
async deletePage(name: string): Promise<void> {
|
2022-04-07 13:21:30 +00:00
|
|
|
await this.getPageMeta(name); // Check if page exists, if not throws Error
|
2022-09-12 12:50:37 +00:00
|
|
|
await this.space.deleteFile(`${name}.md`);
|
2022-04-07 13:21:30 +00:00
|
|
|
|
|
|
|
this.pageMetaCache.delete(name);
|
|
|
|
this.emit("pageDeleted", name);
|
|
|
|
this.emit("pageListUpdated", new Set([...this.pageMetaCache.values()]));
|
|
|
|
}
|
|
|
|
|
|
|
|
async getPageMeta(name: string): Promise<PageMeta> {
|
2022-10-15 17:02:56 +00:00
|
|
|
const oldMeta = this.pageMetaCache.get(name);
|
|
|
|
const newMeta = fileMetaToPageMeta(
|
2022-10-10 16:19:08 +00:00
|
|
|
await this.space.getFileMeta(`${name}.md`),
|
2022-09-12 12:50:37 +00:00
|
|
|
);
|
2022-04-08 15:46:09 +00:00
|
|
|
if (oldMeta) {
|
|
|
|
if (oldMeta.lastModified !== newMeta.lastModified) {
|
|
|
|
// Changed on disk, trigger event
|
|
|
|
this.emit("pageChanged", newMeta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return this.metaCacher(name, newMeta);
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
2022-04-05 15:02:17 +00:00
|
|
|
|
|
|
|
invokeFunction(
|
|
|
|
plug: Plug<any>,
|
|
|
|
env: string,
|
|
|
|
name: string,
|
2022-10-10 16:19:08 +00:00
|
|
|
args: any[],
|
2022-04-07 13:21:30 +00:00
|
|
|
): Promise<any> {
|
|
|
|
return this.space.invokeFunction(plug, env, name, args);
|
|
|
|
}
|
|
|
|
|
2022-09-12 12:50:37 +00:00
|
|
|
listPages(): Set<PageMeta> {
|
|
|
|
return new Set(this.pageMetaCache.values());
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
|
|
|
|
2022-09-12 12:50:37 +00:00
|
|
|
async listPlugs(): Promise<string[]> {
|
2022-10-15 17:02:56 +00:00
|
|
|
const allFiles = await this.space.fetchFileList();
|
2022-09-12 12:50:37 +00:00
|
|
|
return allFiles
|
|
|
|
.filter((fileMeta) => fileMeta.name.endsWith(".plug.json"))
|
|
|
|
.map((fileMeta) => fileMeta.name);
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any> {
|
|
|
|
return this.space.proxySyscall(plug, name, args);
|
|
|
|
}
|
|
|
|
|
|
|
|
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
2022-10-15 17:02:56 +00:00
|
|
|
const pageData = await this.space.readFile(`${name}.md`, "string");
|
|
|
|
const previousMeta = this.pageMetaCache.get(name);
|
|
|
|
const newMeta = fileMetaToPageMeta(pageData.meta);
|
2022-04-08 15:46:09 +00:00
|
|
|
if (previousMeta) {
|
2022-09-12 12:50:37 +00:00
|
|
|
if (previousMeta.lastModified !== newMeta.lastModified) {
|
2022-04-08 15:46:09 +00:00
|
|
|
// Page changed since last cached metadata, trigger event
|
2022-09-12 12:50:37 +00:00
|
|
|
this.emit("pageChanged", newMeta);
|
2022-04-08 15:46:09 +00:00
|
|
|
}
|
|
|
|
}
|
2022-10-15 17:02:56 +00:00
|
|
|
const meta = this.metaCacher(name, newMeta);
|
2022-09-12 12:50:37 +00:00
|
|
|
return {
|
|
|
|
text: pageData.data as string,
|
|
|
|
meta: meta,
|
|
|
|
};
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
watchPage(pageName: string) {
|
|
|
|
this.watchedPages.add(pageName);
|
|
|
|
}
|
|
|
|
|
|
|
|
unwatchPage(pageName: string) {
|
|
|
|
this.watchedPages.delete(pageName);
|
|
|
|
}
|
|
|
|
|
|
|
|
async writePage(
|
|
|
|
name: string,
|
|
|
|
text: string,
|
2022-10-10 16:19:08 +00:00
|
|
|
selfUpdate?: boolean,
|
2022-04-07 13:21:30 +00:00
|
|
|
): Promise<PageMeta> {
|
|
|
|
try {
|
|
|
|
this.saving = true;
|
2022-10-10 16:19:08 +00:00
|
|
|
const pageMeta = fileMetaToPageMeta(
|
|
|
|
await this.space.writeFile(`${name}.md`, "string", text, selfUpdate),
|
2022-04-07 13:21:30 +00:00
|
|
|
);
|
|
|
|
if (!selfUpdate) {
|
|
|
|
this.emit("pageChanged", pageMeta);
|
|
|
|
}
|
|
|
|
return this.metaCacher(name, pageMeta);
|
|
|
|
} finally {
|
|
|
|
this.saving = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-12 12:50:37 +00:00
|
|
|
async fetchPageList(): Promise<PageMeta[]> {
|
|
|
|
return (await this.space.fetchFileList())
|
|
|
|
.filter((fileMeta) => fileMeta.name.endsWith(".md"))
|
|
|
|
.map(fileMetaToPageMeta);
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
|
|
|
|
2022-09-12 12:50:37 +00:00
|
|
|
async fetchAttachmentList(): Promise<AttachmentMeta[]> {
|
|
|
|
return (await this.space.fetchFileList()).filter(
|
|
|
|
(fileMeta) =>
|
2022-12-28 07:51:55 +00:00
|
|
|
!fileMeta.name.endsWith(".md") &&
|
|
|
|
!fileMeta.name.endsWith(".plug.json") &&
|
|
|
|
fileMeta.name !== "data.db",
|
2022-09-12 12:50:37 +00:00
|
|
|
);
|
2022-09-05 09:47:30 +00:00
|
|
|
}
|
2022-09-12 12:50:37 +00:00
|
|
|
|
2023-01-08 14:29:34 +00:00
|
|
|
/**
|
|
|
|
* Reads an attachment
|
|
|
|
* @param name path of the attachment
|
|
|
|
* @param encoding how the return value is expected to be encoded
|
|
|
|
* @returns
|
|
|
|
*/
|
2022-09-05 09:47:30 +00:00
|
|
|
readAttachment(
|
2022-09-05 14:15:01 +00:00
|
|
|
name: string,
|
2022-10-10 16:19:08 +00:00
|
|
|
encoding: FileEncoding,
|
2022-09-12 12:50:37 +00:00
|
|
|
): Promise<{ data: FileData; meta: AttachmentMeta }> {
|
|
|
|
return this.space.readFile(name, encoding);
|
2022-09-05 09:47:30 +00:00
|
|
|
}
|
2022-09-12 12:50:37 +00:00
|
|
|
|
2022-09-05 09:47:30 +00:00
|
|
|
getAttachmentMeta(name: string): Promise<AttachmentMeta> {
|
2022-09-12 12:50:37 +00:00
|
|
|
return this.space.getFileMeta(name);
|
2022-09-05 09:47:30 +00:00
|
|
|
}
|
2022-09-12 12:50:37 +00:00
|
|
|
|
2022-09-05 09:47:30 +00:00
|
|
|
writeAttachment(
|
|
|
|
name: string,
|
2022-09-12 12:50:37 +00:00
|
|
|
encoding: FileEncoding,
|
|
|
|
data: FileData,
|
2022-10-10 16:19:08 +00:00
|
|
|
selfUpdate?: boolean | undefined,
|
2022-09-05 09:47:30 +00:00
|
|
|
): Promise<AttachmentMeta> {
|
2022-09-12 12:50:37 +00:00
|
|
|
return this.space.writeFile(name, encoding, data, selfUpdate);
|
2022-09-05 09:47:30 +00:00
|
|
|
}
|
2022-09-12 12:50:37 +00:00
|
|
|
|
2022-09-05 09:47:30 +00:00
|
|
|
deleteAttachment(name: string): Promise<void> {
|
2022-09-12 12:50:37 +00:00
|
|
|
return this.space.deleteFile(name);
|
2022-09-05 09:47:30 +00:00
|
|
|
}
|
|
|
|
|
2022-09-12 12:50:37 +00:00
|
|
|
private metaCacher(name: string, meta: PageMeta): PageMeta {
|
|
|
|
this.pageMetaCache.set(name, meta);
|
|
|
|
return meta;
|
2022-04-07 13:21:30 +00:00
|
|
|
}
|
2022-04-05 15:02:17 +00:00
|
|
|
}
|
2022-09-12 12:50:37 +00:00
|
|
|
|
|
|
|
function fileMetaToPageMeta(fileMeta: FileMeta): PageMeta {
|
|
|
|
return {
|
|
|
|
...fileMeta,
|
|
|
|
name: fileMeta.name.substring(0, fileMeta.name.length - 3),
|
|
|
|
} as PageMeta;
|
|
|
|
}
|