1
0

More fixes for service workers

This commit is contained in:
Zef Hemel 2022-04-07 15:21:30 +02:00
parent fff2690e99
commit e10f41031c
13 changed files with 298 additions and 337 deletions

View File

@ -152,27 +152,6 @@ export class ExpressServer {
let plugRouter = express.Router();
// Plug list
plugRouter.get("/", async (req, res) => {
res.json(
[...this.system.loadedPlugs.values()].map(({ name, version }) => ({
name,
version,
}))
);
});
plugRouter.get("/:name", async (req, res) => {
const plugName = req.params.name;
const plug = this.system.loadedPlugs.get(plugName);
if (!plug) {
res.status(404);
res.send("Not found");
} else {
res.header("Last-Modified", "" + plug.version);
res.send(plug.manifest);
}
});
plugRouter.post(
"/:plug/syscall/:name",
bodyParser.json(),

View File

@ -1,14 +1,14 @@
import { Editor } from "./editor";
import { safeRun } from "./util";
import { WatchableSpace } from "./spaces/cache_space";
import { HttpRestSpace } from "./spaces/httprest_space";
import { IndexedDBSpace } from "./spaces/indexeddb_space";
import { Space } from "./spaces/space";
import { HttpSpacePrimitives } from "./spaces/http_space_primitives";
import { IndexedDBSpacePrimitives } from "./spaces/indexeddb_space_primitives";
import { SpaceSync } from "./spaces/sync";
let localSpace = new WatchableSpace(new IndexedDBSpace("pages"), true);
let localSpace = new Space(new IndexedDBSpacePrimitives("pages"), true);
localSpace.watch();
let serverSpace = new WatchableSpace(new HttpRestSpace(""), true);
// serverSpace.watch();
let serverSpace = new Space(new HttpSpacePrimitives(""), true);
serverSpace.watch();
// @ts-ignore
window.syncer = async () => {
@ -28,7 +28,7 @@ window.syncer = async () => {
localStorage.setItem("lastRemoteSync", "" + syncer.primaryLastSync);
console.log("Done!");
};
let editor = new Editor(localSpace, document.getElementById("root")!);
let editor = new Editor(serverSpace, document.getElementById("root")!);
safeRun(async () => {
await editor.init();

View File

@ -29,7 +29,7 @@ import { PathPageNavigator } from "./navigator";
import customMarkDown from "./parser";
import reducer from "./reducer";
import { smartQuoteKeymap } from "./smart_quotes";
import { WatchableSpace } from "./spaces/cache_space";
import { Space } from "./spaces/space";
import customMarkdownStyle from "./style";
import { editorSyscalls } from "./syscalls/editor";
import { indexerSyscalls } from "./syscalls";
@ -59,7 +59,7 @@ class PageState {
}
}
const saveInterval = 2000;
const saveInterval = 1000;
export class Editor implements AppEventDispatcher {
readonly commandHook: CommandHook;
@ -69,7 +69,7 @@ export class Editor implements AppEventDispatcher {
editorView?: EditorView;
viewState: AppViewState;
viewDispatch: React.Dispatch<Action>;
space: WatchableSpace;
space: Space;
pageNavigator: PathPageNavigator;
eventHook: EventHook;
saveTimeout: any;
@ -78,7 +78,7 @@ export class Editor implements AppEventDispatcher {
}, 1000);
private system = new System<SilverBulletHooks>("client");
constructor(space: WatchableSpace, parent: Element) {
constructor(space: Space, parent: Element) {
this.space = space;
this.viewState = initialViewState;
this.viewDispatch = () => {};

View File

@ -2,16 +2,43 @@ import { manifest, version } from "@parcel/service-worker";
async function install() {
const cache = await caches.open(version);
// console.log("Installing", manifest);
await cache.addAll(manifest);
// console.log("DOne");
}
//@ts-ignore
self.addEventListener("install", (e) => e.waitUntil(install()));
async function activate() {
const keys = await caches.keys();
// console.log("Activating");
await Promise.all(keys.map((key) => key !== version && caches.delete(key)));
// console.log("DOne activating");
}
//@ts-ignore
self.addEventListener("activate", (e) => e.waitUntil(activate()));
self.addEventListener("fetch", function (event) {});
self.addEventListener("fetch", (event: any) => {
event.respondWith(
caches.open(version).then(async (cache) => {
let parsedUrl = new URL(event.request.url);
// console.log("Got fetch request", parsedUrl.pathname);
let response = await cache.match(event.request, {
ignoreSearch: true,
});
// console.log("Got cache result", response);
if (response) {
return response;
} else {
if (
parsedUrl.pathname !== "/fs" &&
!parsedUrl.pathname.startsWith("/fs/") &&
!parsedUrl.pathname.startsWith("/plug/")
) {
return cache.match("/index.html");
}
return fetch(event.request);
}
})
);
});

View File

@ -1,251 +0,0 @@
import { Space, SpaceEvents } from "./space";
import { safeRun } from "../util";
import { PageMeta } from "../../common/types";
import { EventEmitter } from "../../common/event";
import { Plug } from "../../plugos/plug";
const pageWatchInterval = 2000;
const trashPrefix = "_trash/";
const plugPrefix = "_plug/";
export class WatchableSpace extends EventEmitter<SpaceEvents> {
pageMetaCache = new Map<string, PageMeta>();
watchedPages = new Set<string>();
private initialPageListLoad = true;
private saving = false;
constructor(private space: Space, private trashEnabled = true) {
super();
this.on({
pageCreated: async (pageMeta) => {
if (pageMeta.name.startsWith(plugPrefix)) {
let pageData = await this.readPage(pageMeta.name);
this.emit(
"plugLoaded",
pageMeta.name.substring(plugPrefix.length),
JSON.parse(pageData.text)
);
this.watchPage(pageMeta.name);
}
},
pageChanged: async (pageMeta) => {
if (pageMeta.name.startsWith(plugPrefix)) {
let pageData = await this.readPage(pageMeta.name);
this.emit(
"plugLoaded",
pageMeta.name.substring(plugPrefix.length),
JSON.parse(pageData.text)
);
this.watchPage(pageMeta.name);
}
},
});
}
public updatePageListAsync() {
safeRun(async () => {
let newPageList = await this.space.fetchPageList();
let deletedPages = new Set<string>(this.pageMetaCache.keys());
newPageList.pages.forEach((meta) => {
const pageName = meta.name;
const oldPageMeta = this.pageMetaCache.get(pageName);
const newPageMeta = {
name: pageName,
lastModified: meta.lastModified,
};
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;
});
}
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;
}
const newMeta = await this.space.getPageMeta(pageName);
if (oldMeta.lastModified !== newMeta.lastModified) {
this.emit("pageChanged", newMeta);
}
}
});
}, pageWatchInterval);
this.updatePageListAsync();
}
async deletePage(name: string, deleteDate?: number): Promise<void> {
await this.getPageMeta(name); // Check if page exists, if not throws Error
if (this.trashEnabled) {
let pageData = await this.readPage(name);
// Move to trash
await this.writePage(
`${trashPrefix}${name}`,
pageData.text,
false,
deleteDate
);
}
await this.space.deletePage(name);
this.pageMetaCache.delete(name);
this.emit("pageDeleted", name);
this.emit("pageListUpdated", new Set([...this.pageMetaCache.values()]));
}
async getPageMeta(name: string): Promise<PageMeta> {
return this.metaCacher(name, await this.space.getPageMeta(name));
}
invokeFunction(
plug: Plug<any>,
env: string,
name: string,
args: any[]
): Promise<any> {
return this.space.invokeFunction(plug, env, name, args);
}
listPages(): Set<PageMeta> {
return new Set(
[...this.pageMetaCache.values()].filter(
(pageMeta) =>
!pageMeta.name.startsWith(trashPrefix) &&
!pageMeta.name.startsWith(plugPrefix)
)
);
}
listTrash(): Set<PageMeta> {
return new Set(
[...this.pageMetaCache.values()]
.filter(
(pageMeta) =>
pageMeta.name.startsWith(trashPrefix) &&
!pageMeta.name.startsWith(plugPrefix)
)
.map((pageMeta) => ({
...pageMeta,
name: pageMeta.name.substring(trashPrefix.length),
}))
);
}
listPlugs(): Set<PageMeta> {
return new Set(
[...this.pageMetaCache.values()].filter((pageMeta) =>
pageMeta.name.startsWith(plugPrefix)
)
);
}
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 }> {
let pageData = await this.space.readPage(name);
this.pageMetaCache.set(name, pageData.meta);
return pageData;
}
watchPage(pageName: string) {
this.watchedPages.add(pageName);
}
unwatchPage(pageName: string) {
this.watchedPages.delete(pageName);
}
async writePage(
name: string,
text: string,
selfUpdate?: boolean,
lastModified?: number
): Promise<PageMeta> {
try {
this.saving = true;
let pageMeta = await this.space.writePage(
name,
text,
selfUpdate,
lastModified
);
if (!selfUpdate) {
this.emit("pageChanged", pageMeta);
}
return this.metaCacher(name, pageMeta);
} finally {
this.saving = false;
}
}
fetchPageList(): Promise<{ pages: Set<PageMeta>; nowTimestamp: number }> {
return this.space.fetchPageList();
}
// private pollPlugs() {
// safeRun(async () => {
// const newPlugs = await this.space.listPlugs();
// let deletedPlugs = new Set<string>(this.plugMetaCache.keys());
// for (const newPlugMeta of newPlugs) {
// const oldPlugMeta = this.plugMetaCache.get(newPlugMeta.name);
// if (
// !oldPlugMeta ||
// (oldPlugMeta && oldPlugMeta.version !== newPlugMeta.version)
// ) {
// this.emit(
// "plugLoaded",
// newPlugMeta.name,
// await this.space.loadPlug(newPlugMeta.name)
// );
// }
// // Page found, not deleted
// deletedPlugs.delete(newPlugMeta.name);
//
// // Update in cache
// this.plugMetaCache.set(newPlugMeta.name, newPlugMeta);
// }
//
// for (const deletedPlug of deletedPlugs) {
// this.plugMetaCache.delete(deletedPlug);
// this.emit("plugUnloaded", deletedPlug);
// }
// });
// }
private metaCacher(name: string, pageMeta: PageMeta): PageMeta {
this.pageMetaCache.set(name, pageMeta);
return pageMeta;
}
}

View File

@ -1,8 +1,8 @@
import { PageMeta } from "../../common/types";
import { Plug } from "../../plugos/plug";
import { Space } from "./space";
import { SpacePrimitives } from "./space_primitives";
export class HttpRestSpace implements Space {
export class HttpSpacePrimitives implements SpacePrimitives {
pageUrl: string;
private plugUrl: string;

View File

@ -1,4 +1,4 @@
import { Space } from "./space";
import { SpacePrimitives } from "./space_primitives";
import { PageMeta } from "../../common/types";
import Dexie, { Table } from "dexie";
import { Plug } from "../../plugos/plug";
@ -9,7 +9,7 @@ type Page = {
meta: PageMeta;
};
export class IndexedDBSpace implements Space {
export class IndexedDBSpacePrimitives implements SpacePrimitives {
private pageTable: Table<Page, string>;
constructor(dbName: string, readonly timeSkew: number = 0) {
@ -54,7 +54,6 @@ export class IndexedDBSpace implements Space {
}
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any> {
console.log("Going this", name);
return plug.syscall(name, args);
}

View File

@ -1,6 +1,13 @@
import { Manifest } from "../../common/manifest";
import { Plug } from "../../plugos/plug";
import { SpacePrimitives } from "./space_primitives";
import { safeRun } from "../util";
import { PageMeta } from "../../common/types";
import { EventEmitter } from "../../common/event";
import { Plug } from "../../plugos/plug";
import { Manifest } from "../../common/manifest";
const pageWatchInterval = 2000;
const trashPrefix = "_trash/";
const plugPrefix = "_plug/";
export type SpaceEvents = {
pageCreated: (meta: PageMeta) => void;
@ -11,25 +18,214 @@ export type SpaceEvents = {
plugUnloaded: (plugName: string) => void;
};
export interface Space {
// Pages
fetchPageList(): Promise<{ pages: Set<PageMeta>; nowTimestamp: number }>;
readPage(name: string): Promise<{ text: string; meta: PageMeta }>;
getPageMeta(name: string): Promise<PageMeta>;
writePage(
name: string,
text: string,
selfUpdate?: boolean,
lastModified?: number
): Promise<PageMeta>;
deletePage(name: string): Promise<void>;
export class Space extends EventEmitter<SpaceEvents> {
pageMetaCache = new Map<string, PageMeta>();
watchedPages = new Set<string>();
private initialPageListLoad = true;
private saving = false;
constructor(private space: SpacePrimitives, private trashEnabled = true) {
super();
this.on({
pageCreated: async (pageMeta) => {
if (pageMeta.name.startsWith(plugPrefix)) {
let pageData = await this.readPage(pageMeta.name);
this.emit(
"plugLoaded",
pageMeta.name.substring(plugPrefix.length),
JSON.parse(pageData.text)
);
this.watchPage(pageMeta.name);
}
},
pageChanged: async (pageMeta) => {
if (pageMeta.name.startsWith(plugPrefix)) {
let pageData = await this.readPage(pageMeta.name);
this.emit(
"plugLoaded",
pageMeta.name.substring(plugPrefix.length),
JSON.parse(pageData.text)
);
this.watchPage(pageMeta.name);
}
},
});
}
public updatePageListAsync() {
safeRun(async () => {
let newPageList = await this.space.fetchPageList();
let deletedPages = new Set<string>(this.pageMetaCache.keys());
newPageList.pages.forEach((meta) => {
const pageName = meta.name;
const oldPageMeta = this.pageMetaCache.get(pageName);
const newPageMeta = {
name: pageName,
lastModified: meta.lastModified,
};
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;
});
}
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;
}
const newMeta = await this.space.getPageMeta(pageName);
if (oldMeta.lastModified !== newMeta.lastModified) {
this.emit("pageChanged", newMeta);
}
}
});
}, pageWatchInterval);
this.updatePageListAsync();
}
async deletePage(name: string, deleteDate?: number): Promise<void> {
await this.getPageMeta(name); // Check if page exists, if not throws Error
if (this.trashEnabled) {
let pageData = await this.readPage(name);
// Move to trash
await this.writePage(
`${trashPrefix}${name}`,
pageData.text,
false,
deleteDate
);
}
await this.space.deletePage(name);
this.pageMetaCache.delete(name);
this.emit("pageDeleted", name);
this.emit("pageListUpdated", new Set([...this.pageMetaCache.values()]));
}
async getPageMeta(name: string): Promise<PageMeta> {
return this.metaCacher(name, await this.space.getPageMeta(name));
}
// Plugs
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any>;
invokeFunction(
plug: Plug<any>,
env: string,
name: string,
args: any[]
): Promise<any>;
): Promise<any> {
return this.space.invokeFunction(plug, env, name, args);
}
listPages(): Set<PageMeta> {
return new Set(
[...this.pageMetaCache.values()].filter(
(pageMeta) =>
!pageMeta.name.startsWith(trashPrefix) &&
!pageMeta.name.startsWith(plugPrefix)
)
);
}
listTrash(): Set<PageMeta> {
return new Set(
[...this.pageMetaCache.values()]
.filter(
(pageMeta) =>
pageMeta.name.startsWith(trashPrefix) &&
!pageMeta.name.startsWith(plugPrefix)
)
.map((pageMeta) => ({
...pageMeta,
name: pageMeta.name.substring(trashPrefix.length),
}))
);
}
listPlugs(): Set<PageMeta> {
return new Set(
[...this.pageMetaCache.values()].filter((pageMeta) =>
pageMeta.name.startsWith(plugPrefix)
)
);
}
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 }> {
let pageData = await this.space.readPage(name);
this.pageMetaCache.set(name, pageData.meta);
return pageData;
}
watchPage(pageName: string) {
this.watchedPages.add(pageName);
}
unwatchPage(pageName: string) {
this.watchedPages.delete(pageName);
}
async writePage(
name: string,
text: string,
selfUpdate?: boolean,
lastModified?: number
): Promise<PageMeta> {
try {
this.saving = true;
let pageMeta = await this.space.writePage(
name,
text,
selfUpdate,
lastModified
);
if (!selfUpdate) {
this.emit("pageChanged", pageMeta);
}
return this.metaCacher(name, pageMeta);
} finally {
this.saving = false;
}
}
fetchPageList(): Promise<{ pages: Set<PageMeta>; nowTimestamp: number }> {
return this.space.fetchPageList();
}
private metaCacher(name: string, pageMeta: PageMeta): PageMeta {
this.pageMetaCache.set(name, pageMeta);
return pageMeta;
}
}

View File

@ -0,0 +1,25 @@
import { Plug } from "../../plugos/plug";
import { PageMeta } from "../../common/types";
export interface SpacePrimitives {
// Pages
fetchPageList(): Promise<{ pages: Set<PageMeta>; nowTimestamp: number }>;
readPage(name: string): Promise<{ text: string; meta: PageMeta }>;
getPageMeta(name: string): Promise<PageMeta>;
writePage(
name: string,
text: string,
selfUpdate?: boolean,
lastModified?: number
): Promise<PageMeta>;
deletePage(name: string): Promise<void>;
// Plugs
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any>;
invokeFunction(
plug: Plug<any>,
env: string,
name: string,
args: any[]
): Promise<any>;
}

View File

@ -1,16 +1,16 @@
import { expect, test } from "@jest/globals";
import { IndexedDBSpace } from "./indexeddb_space";
import { IndexedDBSpacePrimitives } from "./indexeddb_space_primitives";
import { SpaceSync } from "./sync";
import { PageMeta } from "../../common/types";
import { WatchableSpace } from "./cache_space";
import { Space } from "./space";
// For testing in node.js
require("fake-indexeddb/auto");
test("Test store", async () => {
let primary = new WatchableSpace(new IndexedDBSpace("primary"), true);
let secondary = new WatchableSpace(
new IndexedDBSpace("secondary", -5000),
let primary = new Space(new IndexedDBSpacePrimitives("primary"), true);
let secondary = new Space(
new IndexedDBSpacePrimitives("secondary", -5000),
true
);
let sync = new SpaceSync(primary, secondary, 0, 0, "_trash/");

View File

@ -1,11 +1,11 @@
import { WatchableSpace } from "./cache_space";
import { PageMeta } from "../../common/types";
import { Space } from "./space";
import { PageMeta } from "../../common/types";
import { SpacePrimitives } from "./space_primitives";
export class SpaceSync {
constructor(
private primary: WatchableSpace,
private secondary: WatchableSpace,
private primary: Space,
private secondary: Space,
public primaryLastSync: number,
public secondaryLastSync: number,
private trashPrefix: string
@ -13,8 +13,8 @@ export class SpaceSync {
// Strategy: Primary wins
public static primaryConflictResolver(
primary: WatchableSpace,
secondary: WatchableSpace
primary: Space,
secondary: Space
): (pageMeta1: PageMeta, pageMeta2: PageMeta) => Promise<void> {
return async (pageMeta1, pageMeta2) => {
const pageName = pageMeta1.name;
@ -35,7 +35,7 @@ export class SpaceSync {
}
async syncablePages(
space: WatchableSpace
space: Space
): Promise<{ pages: PageMeta[]; nowTimestamp: number }> {
let fetchResult = await space.fetchPageList();
return {
@ -46,7 +46,7 @@ export class SpaceSync {
};
}
async trashPages(space: Space): Promise<PageMeta[]> {
async trashPages(space: SpacePrimitives): Promise<PageMeta[]> {
return [...(await space.fetchPageList()).pages]
.filter((pageMeta) => pageMeta.name.startsWith(this.trashPrefix))
.map((pageMeta) => ({
@ -203,20 +203,6 @@ export class SpaceSync {
this.primaryLastSync = primarySyncTimestamp;
this.secondaryLastSync = secondarySyncTimestamp;
// Find the latest timestamp and set it as lastSync
// allPagesPrimary.forEach((pageMeta) => {
// this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
// });
// allPagesSecondary.forEach((pageMeta) => {
// this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
// });
// allTrashPrimary.forEach((pageMeta) => {
// this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
// });
// allTrashSecondary.forEach((pageMeta) => {
// this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
// });
return syncOps;
}
}

View File

@ -1,8 +1,8 @@
import { SysCallMapping } from "../../plugos/system";
import { proxySyscalls } from "../../plugos/syscalls/transport";
import { WatchableSpace } from "../spaces/cache_space";
import { Space } from "../spaces/space";
export function indexerSyscalls(space: WatchableSpace): SysCallMapping {
export function indexerSyscalls(space: Space): SysCallMapping {
return proxySyscalls(
[
"index.scanPrefixForPage",

View File

@ -1,7 +1,7 @@
import { SysCallMapping } from "../../plugos/system";
import { WatchableSpace } from "../spaces/cache_space";
import { Space } from "../spaces/space";
export function systemSyscalls(space: WatchableSpace): SysCallMapping {
export function systemSyscalls(space: Space): SysCallMapping {
return {
"system.invokeFunction": async (
ctx,