Tons of work
This commit is contained in:
parent
da4bf4a9ab
commit
1984d8eefe
13
plugbox/jest.config.js
Normal file
13
plugbox/jest.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export default {
|
||||||
|
extensionsToTreatAsEsm: [".ts"],
|
||||||
|
preset: "ts-jest/presets/default-esm", // or other ESM presets
|
||||||
|
globals: {
|
||||||
|
"ts-jest": {
|
||||||
|
useESM: true,
|
||||||
|
tsconfig: "<rootDir>/tsconfig.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||||
|
},
|
||||||
|
};
|
@ -7,17 +7,30 @@
|
|||||||
"plugbox-bundle": "./bin/plugbox-bundle.mjs"
|
"plugbox-bundle": "./bin/plugbox-bundle.mjs"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check": "tsc --noEmit"
|
"check": "tsc --noEmit",
|
||||||
|
"test": "jest",
|
||||||
|
"build-worker": "tsc src/node_worker.ts --outDir dist --module nodenext"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.14.24",
|
"esbuild": "^0.14.24",
|
||||||
"idb": "^7.0.0",
|
"idb": "^7.0.0",
|
||||||
"typescript": ">=3.0.0",
|
"typescript": "^4.7.0-dev.20220313",
|
||||||
"vm2": "^3.9.9",
|
"vm2": "^3.9.9",
|
||||||
"yargs": "^17.3.1"
|
"yargs": "^17.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@jest/globals": "^27.5.1",
|
||||||
|
"@types/jest": "^27.4.1",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/yargs": "^17.0.9"
|
"@types/yargs": "^17.0.9",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"jest": "^27.5.1",
|
||||||
|
"parcel": "^2.3.2",
|
||||||
|
"parceljs": "^0.0.1",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"ts-jest": "^27.1.3",
|
||||||
|
"util": "^0.12.4",
|
||||||
|
"vm-browserify": "^1.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
92
plugbox/src/node_sandbox.ts
Normal file
92
plugbox/src/node_sandbox.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { ControllerMessage, WorkerMessage } from "./types";
|
||||||
|
import { System, Sandbox } from "./runtime";
|
||||||
|
|
||||||
|
import { Worker } from "worker_threads";
|
||||||
|
|
||||||
|
function wrapScript(code: string): string {
|
||||||
|
return `${code}["default"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NodeSandbox implements Sandbox {
|
||||||
|
worker: Worker;
|
||||||
|
private reqId = 0;
|
||||||
|
|
||||||
|
outstandingInits = new Map<string, () => void>();
|
||||||
|
outstandingInvocations = new Map<
|
||||||
|
number,
|
||||||
|
{ resolve: (result: any) => void; reject: (e: any) => void }
|
||||||
|
>();
|
||||||
|
loadedFunctions = new Set<string>();
|
||||||
|
|
||||||
|
constructor(readonly system: System<any>, workerScript: string) {
|
||||||
|
this.worker = new Worker(workerScript);
|
||||||
|
|
||||||
|
this.worker.on("message", this.onmessage.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoaded(name: string): boolean {
|
||||||
|
return this.loadedFunctions.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(name: string, code: string): Promise<void> {
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: "load",
|
||||||
|
name: name,
|
||||||
|
code: code,
|
||||||
|
} as WorkerMessage);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.loadedFunctions.add(name);
|
||||||
|
this.outstandingInits.set(name, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onmessage(data: ControllerMessage) {
|
||||||
|
// let data = evt.data;
|
||||||
|
// let data = JSON.parse(msg) as ControllerMessage;
|
||||||
|
switch (data.type) {
|
||||||
|
case "inited":
|
||||||
|
let initCb = this.outstandingInits.get(data.name!);
|
||||||
|
initCb && initCb();
|
||||||
|
this.outstandingInits.delete(data.name!);
|
||||||
|
break;
|
||||||
|
case "syscall":
|
||||||
|
let result = await this.system.syscall(data.name!, data.args!);
|
||||||
|
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: "syscall-response",
|
||||||
|
id: data.id,
|
||||||
|
data: result,
|
||||||
|
} as WorkerMessage);
|
||||||
|
break;
|
||||||
|
case "result":
|
||||||
|
let resultCb = this.outstandingInvocations.get(data.id!);
|
||||||
|
this.outstandingInvocations.delete(data.id!);
|
||||||
|
resultCb && resultCb.resolve(data.result);
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
let errCb = this.outstandingInvocations.get(data.result.id!);
|
||||||
|
this.outstandingInvocations.delete(data.id!);
|
||||||
|
errCb && errCb.reject(data.reason);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Unknown message type", data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async invoke(name: string, args: any[]): Promise<any> {
|
||||||
|
this.reqId++;
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: "invoke",
|
||||||
|
id: this.reqId,
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.outstandingInvocations.set(this.reqId, { resolve, reject });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.worker.terminate();
|
||||||
|
}
|
||||||
|
}
|
88
plugbox/src/node_worker.ts
Normal file
88
plugbox/src/node_worker.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { VM, VMScript } from "vm2";
|
||||||
|
import { parentPort } from "worker_threads";
|
||||||
|
|
||||||
|
let loadedFunctions = new Map();
|
||||||
|
let pendingRequests = new Map();
|
||||||
|
|
||||||
|
let reqId = 0; // Syscall request ID
|
||||||
|
|
||||||
|
let vm = new VM({
|
||||||
|
sandbox: {
|
||||||
|
console: console,
|
||||||
|
syscall: (name: string, args: any[]) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reqId++;
|
||||||
|
pendingRequests.set(reqId, resolve);
|
||||||
|
parentPort!.postMessage({
|
||||||
|
type: "syscall",
|
||||||
|
id: reqId,
|
||||||
|
name,
|
||||||
|
// TODO: Figure out why this is necessary (to avoide a CloneError)
|
||||||
|
args: JSON.parse(JSON.stringify(args)),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function wrapScript(code: string) {
|
||||||
|
return `${code}["default"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeRun(fn: () => Promise<any>) {
|
||||||
|
fn().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPort!.on("message", (data) => {
|
||||||
|
safeRun(async () => {
|
||||||
|
switch (data.type) {
|
||||||
|
case "load":
|
||||||
|
console.log("Booting", data.name);
|
||||||
|
loadedFunctions.set(data.name, new VMScript(wrapScript(data.code)));
|
||||||
|
parentPort!.postMessage({
|
||||||
|
type: "inited",
|
||||||
|
name: data.name,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "invoke":
|
||||||
|
let fn = loadedFunctions.get(data.name);
|
||||||
|
if (!fn) {
|
||||||
|
throw new Error(`Function not loaded: ${data.name}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let r = vm.run(fn);
|
||||||
|
let result = await Promise.resolve(r(...data.args));
|
||||||
|
parentPort!.postMessage({
|
||||||
|
type: "result",
|
||||||
|
id: data.id,
|
||||||
|
result: result,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
parentPort!.postMessage({
|
||||||
|
type: "error",
|
||||||
|
id: data.id,
|
||||||
|
reason: e.message,
|
||||||
|
});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "syscall-response":
|
||||||
|
let syscallId = data.id;
|
||||||
|
const lookup = pendingRequests.get(syscallId);
|
||||||
|
if (!lookup) {
|
||||||
|
console.log(
|
||||||
|
"Current outstanding requests",
|
||||||
|
pendingRequests,
|
||||||
|
"looking up",
|
||||||
|
syscallId
|
||||||
|
);
|
||||||
|
throw Error("Invalid request id");
|
||||||
|
}
|
||||||
|
pendingRequests.delete(syscallId);
|
||||||
|
lookup(data.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
47
plugbox/src/runtime.test.ts
Normal file
47
plugbox/src/runtime.test.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { NodeSandbox } from "./node_sandbox";
|
||||||
|
import { System } from "./runtime";
|
||||||
|
import { test, expect } from "@jest/globals";
|
||||||
|
|
||||||
|
test("Run a Node sandbox", async () => {
|
||||||
|
let system = new System();
|
||||||
|
system.registerSyscalls({
|
||||||
|
addNumbers: (a, b) => {
|
||||||
|
return a + b;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let plug = await system.load(
|
||||||
|
"test",
|
||||||
|
{
|
||||||
|
functions: {
|
||||||
|
addTen: {
|
||||||
|
code: `(() => {
|
||||||
|
return {
|
||||||
|
default: (n) => {
|
||||||
|
return n + 10;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})()`,
|
||||||
|
},
|
||||||
|
addNumbersSyscall: {
|
||||||
|
code: `(() => {
|
||||||
|
return {
|
||||||
|
default: async (a, b) => {
|
||||||
|
return await(syscall("addNumbers", [a, b]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})()`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
events: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new NodeSandbox(system, __dirname + "/../dist/node_worker.js")
|
||||||
|
);
|
||||||
|
expect(await plug.invoke("addTen", [10])).toBe(20);
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
expect(await plug.invoke("addNumbersSyscall", [10, i])).toBe(10 + i);
|
||||||
|
}
|
||||||
|
console.log(plug.sandbox);
|
||||||
|
await system.stop();
|
||||||
|
});
|
@ -1,9 +1,8 @@
|
|||||||
import { Manifest } from "./types";
|
import { Manifest } from "./types";
|
||||||
import { WebworkerSandbox } from "./worker_sandbox";
|
// import { WebworkerSandbox } from "./worker_sandbox";
|
||||||
|
|
||||||
interface SysCallMapping {
|
interface SysCallMapping {
|
||||||
// TODO: Better typing
|
[key: string]: (...args: any) => Promise<any> | any;
|
||||||
[key: string]: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Sandbox {
|
export interface Sandbox {
|
||||||
@ -13,19 +12,14 @@ export interface Sandbox {
|
|||||||
stop(): void;
|
stop(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlugLoader<HookT> {
|
|
||||||
load(name: string, manifest: Manifest<HookT>): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Plug<HookT> {
|
export class Plug<HookT> {
|
||||||
system: System<HookT>;
|
system: System<HookT>;
|
||||||
// private runningFunctions: Map<string, FunctionWorker>;
|
sandbox: Sandbox;
|
||||||
functionWorker: WebworkerSandbox;
|
|
||||||
public manifest?: Manifest<HookT>;
|
public manifest?: Manifest<HookT>;
|
||||||
|
|
||||||
constructor(system: System<HookT>, name: string) {
|
constructor(system: System<HookT>, name: string, sandbox: Sandbox) {
|
||||||
this.system = system;
|
this.system = system;
|
||||||
this.functionWorker = new WebworkerSandbox(this);
|
this.sandbox = sandbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(manifest: Manifest<HookT>) {
|
async load(manifest: Manifest<HookT>) {
|
||||||
@ -34,13 +28,11 @@ export class Plug<HookT> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async invoke(name: string, args: Array<any>): Promise<any> {
|
async invoke(name: string, args: Array<any>): Promise<any> {
|
||||||
if (!this.functionWorker.isLoaded(name)) {
|
if (!this.sandbox.isLoaded(name)) {
|
||||||
await this.functionWorker.load(
|
await this.sandbox.load(name, this.manifest!.functions[name].code!);
|
||||||
name,
|
|
||||||
this.manifest!.functions[name].code!
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return await this.functionWorker.invoke(name, args);
|
console.log("Loaded", name);
|
||||||
|
return await this.sandbox.invoke(name, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispatchEvent(name: string, data?: any): Promise<any[]> {
|
async dispatchEvent(name: string, data?: any): Promise<any[]> {
|
||||||
@ -58,18 +50,13 @@ export class Plug<HookT> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
this.functionWorker.stop();
|
this.sandbox.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class System<HookT> {
|
export class System<HookT> {
|
||||||
protected plugs: Map<string, Plug<HookT>>;
|
protected plugs = new Map<string, Plug<HookT>>();
|
||||||
registeredSyscalls: SysCallMapping;
|
registeredSyscalls: SysCallMapping = {};
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.plugs = new Map<string, Plug<HookT>>();
|
|
||||||
this.registeredSyscalls = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
registerSyscalls(...registrationObjects: SysCallMapping[]) {
|
registerSyscalls(...registrationObjects: SysCallMapping[]) {
|
||||||
for (const registrationObject of registrationObjects) {
|
for (const registrationObject of registrationObjects) {
|
||||||
@ -90,8 +77,12 @@ export class System<HookT> {
|
|||||||
return Promise.resolve(callback(...args));
|
return Promise.resolve(callback(...args));
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(name: string, manifest: Manifest<HookT>): Promise<Plug<HookT>> {
|
async load(
|
||||||
const plug = new Plug(this, name);
|
name: string,
|
||||||
|
manifest: Manifest<HookT>,
|
||||||
|
sandbox: Sandbox
|
||||||
|
): Promise<Plug<HookT>> {
|
||||||
|
const plug = new Plug(this, name, sandbox);
|
||||||
await plug.load(manifest);
|
await plug.load(manifest);
|
||||||
this.plugs.set(name, plug);
|
this.plugs.set(name, plug);
|
||||||
return plug;
|
return plug;
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
declare global {
|
|
||||||
function syscall(id: number, name: string, args: any[]): Promise<any>;
|
|
||||||
}
|
|
||||||
import { ControllerMessage, WorkerMessage, WorkerMessageType } from "./types";
|
import { ControllerMessage, WorkerMessage, WorkerMessageType } from "./types";
|
||||||
import { safeRun } from "./util";
|
import { safeRun } from "./util";
|
||||||
|
|
||||||
let loadedFunctions = new Map<string, Function>();
|
let loadedFunctions = new Map<string, Function>();
|
||||||
let pendingRequests = new Map<number, (result: unknown) => void>();
|
let pendingRequests = new Map<number, (result: unknown) => void>();
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
function syscall(id: number, name: string, args: any[]): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
self.syscall = async (id: number, name: string, args: any[]) => {
|
self.syscall = async (id: number, name: string, args: any[]) => {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
pendingRequests.set(id, resolve);
|
pendingRequests.set(id, resolve);
|
||||||
@ -19,22 +20,6 @@ self.syscall = async (id: number, name: string, args: any[]) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
self.addEventListener("result", (event) => {
|
|
||||||
let customEvent = event as CustomEvent;
|
|
||||||
self.postMessage({
|
|
||||||
type: "result",
|
|
||||||
result: customEvent.detail,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("app-error", (event) => {
|
|
||||||
let customEvent = event as CustomEvent;
|
|
||||||
self.postMessage({
|
|
||||||
type: "error",
|
|
||||||
reason: customEvent.detail,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function wrapScript(code: string): string {
|
function wrapScript(code: string): string {
|
||||||
return `const fn = ${code};
|
return `const fn = ${code};
|
||||||
return fn["default"].apply(null, arguments);`;
|
return fn["default"].apply(null, arguments);`;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ControllerMessage, WorkerMessage } from "./types";
|
import { ControllerMessage, WorkerMessage } from "./types";
|
||||||
import { Plug, Sandbox } from "./runtime";
|
import { Plug, Sandbox, System } from "./runtime";
|
||||||
|
|
||||||
export class WebworkerSandbox implements Sandbox {
|
export class WebworkerSandbox implements Sandbox {
|
||||||
private worker: Worker;
|
private worker: Worker;
|
||||||
@ -12,7 +12,7 @@ export class WebworkerSandbox implements Sandbox {
|
|||||||
>();
|
>();
|
||||||
private loadedFunctions = new Set<string>();
|
private loadedFunctions = new Set<string>();
|
||||||
|
|
||||||
constructor(readonly plug: Plug<any>) {
|
constructor(readonly system: System<any>) {
|
||||||
this.worker = new Worker(new URL("sandbox_worker.ts", import.meta.url), {
|
this.worker = new Worker(new URL("sandbox_worker.ts", import.meta.url), {
|
||||||
type: "module",
|
type: "module",
|
||||||
});
|
});
|
||||||
@ -46,7 +46,7 @@ export class WebworkerSandbox implements Sandbox {
|
|||||||
this.outstandingInits.delete(data.name!);
|
this.outstandingInits.delete(data.name!);
|
||||||
break;
|
break;
|
||||||
case "syscall":
|
case "syscall":
|
||||||
let result = await this.plug.system.syscall(data.name!, data.args!);
|
let result = await this.system.syscall(data.name!, data.args!);
|
||||||
|
|
||||||
this.worker.postMessage({
|
this.worker.postMessage({
|
||||||
type: "syscall-response",
|
type: "syscall-response",
|
||||||
@ -56,10 +56,12 @@ export class WebworkerSandbox implements Sandbox {
|
|||||||
break;
|
break;
|
||||||
case "result":
|
case "result":
|
||||||
let resultCb = this.outstandingInvocations.get(data.id!);
|
let resultCb = this.outstandingInvocations.get(data.id!);
|
||||||
|
this.outstandingInvocations.delete(data.id!);
|
||||||
resultCb && resultCb.resolve(data.result);
|
resultCb && resultCb.resolve(data.result);
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
let errCb = this.outstandingInvocations.get(data.result.id!);
|
let errCb = this.outstandingInvocations.get(data.result.id!);
|
||||||
|
this.outstandingInvocations.delete(data.id!);
|
||||||
errCb && errCb.reject(data.reason);
|
errCb && errCb.reject(data.reason);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"module": "ESNext",
|
"module": "esnext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true
|
||||||
}
|
}
|
||||||
|
3059
plugbox/yarn.lock
3059
plugbox/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,15 @@
|
|||||||
declare global {
|
declare global {
|
||||||
function syscall(id: number, name: string, args: any[]): Promise<any>;
|
function syscall(id: number, name: string, args: any[]): Promise<any>;
|
||||||
|
var reqId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This needs to be global, because this will be shared with all other functions in the same environment (worker-like)
|
||||||
|
if (typeof self.reqId === "undefined") {
|
||||||
|
self.reqId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syscall(name: string, ...args: any[]): Promise<any> {
|
export async function syscall(name: string, ...args: any[]): Promise<any> {
|
||||||
let reqId = Math.floor(Math.random() * 1000000);
|
self.reqId++;
|
||||||
// console.log("Syscall", name, reqId);
|
// console.log("Syscall", name, reqId);
|
||||||
return await self.syscall(reqId, name, args);
|
return await self.syscall(self.reqId, name, args);
|
||||||
// return new Promise((resolve, reject) => {
|
|
||||||
// self.dispatchEvent(
|
|
||||||
// new CustomEvent("syscall", {
|
|
||||||
// detail: {
|
|
||||||
// id: reqId,
|
|
||||||
// name: name,
|
|
||||||
// args: args,
|
|
||||||
// callback: resolve,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ async function navigate(syntaxNode: any) {
|
|||||||
console.log("Attempting to navigate based on syntax node", syntaxNode);
|
console.log("Attempting to navigate based on syntax node", syntaxNode);
|
||||||
switch (syntaxNode.name) {
|
switch (syntaxNode.name) {
|
||||||
case "WikiLinkPage":
|
case "WikiLinkPage":
|
||||||
|
case "AtMention":
|
||||||
await syscall("editor.navigate", syntaxNode.text);
|
await syscall("editor.navigate", syntaxNode.text);
|
||||||
break;
|
break;
|
||||||
case "URL":
|
case "URL":
|
||||||
@ -35,16 +36,31 @@ export async function clickNavigate(event: ClickEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function pageComplete() {
|
export async function pageComplete() {
|
||||||
let prefix = await syscall("editor.matchBefore", "\\[\\[[\\w\\s]*");
|
let prefix = await syscall(
|
||||||
|
"editor.matchBefore",
|
||||||
|
"(\\[\\[[\\w\\s]*|@[\\w\\.]*)"
|
||||||
|
);
|
||||||
if (!prefix) {
|
if (!prefix) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let allPages = await syscall("space.listPages");
|
let allPages = await syscall("space.listPages");
|
||||||
return {
|
if (prefix.text[0] === "@") {
|
||||||
from: prefix.from + 2,
|
return {
|
||||||
options: allPages.map((pageMeta: any) => ({
|
from: prefix.from,
|
||||||
label: pageMeta.name,
|
options: allPages
|
||||||
type: "page",
|
.filter((page) => page.name.startsWith(prefix.text))
|
||||||
})),
|
.map((pageMeta: any) => ({
|
||||||
};
|
label: pageMeta.name,
|
||||||
|
type: "page",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
from: prefix.from + 2,
|
||||||
|
options: allPages.map((pageMeta: any) => ({
|
||||||
|
label: pageMeta.name,
|
||||||
|
type: "page",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,11 @@ import { pageLinkRegex } from "../../webapp/src/constant";
|
|||||||
import { syscall } from "./lib/syscall";
|
import { syscall } from "./lib/syscall";
|
||||||
|
|
||||||
const wikilinkRegex = new RegExp(pageLinkRegex, "g");
|
const wikilinkRegex = new RegExp(pageLinkRegex, "g");
|
||||||
|
const atMentionRegex = /(@[A-Za-z\.]+)/g;
|
||||||
|
|
||||||
export async function indexLinks({ name, text }: IndexEvent) {
|
export async function indexLinks({ name, text }: IndexEvent) {
|
||||||
let backLinks: { key: string; value: string }[] = [];
|
let backLinks: { key: string; value: string }[] = [];
|
||||||
|
// [[Style Links]]
|
||||||
for (let match of text.matchAll(wikilinkRegex)) {
|
for (let match of text.matchAll(wikilinkRegex)) {
|
||||||
let toPage = match[1];
|
let toPage = match[1];
|
||||||
let pos = match.index!;
|
let pos = match.index!;
|
||||||
@ -14,6 +16,15 @@ export async function indexLinks({ name, text }: IndexEvent) {
|
|||||||
value: name,
|
value: name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// @links
|
||||||
|
for (let match of text.matchAll(atMentionRegex)) {
|
||||||
|
let toPage = match[1];
|
||||||
|
let pos = match.index!;
|
||||||
|
backLinks.push({
|
||||||
|
key: `pl:${toPage}:${pos}`,
|
||||||
|
value: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
console.log("Found", backLinks.length, "wiki link(s)");
|
console.log("Found", backLinks.length, "wiki link(s)");
|
||||||
// throw Error("Boom");
|
// throw Error("Boom");
|
||||||
await syscall("indexer.batchSet", name, backLinks);
|
await syscall("indexer.batchSet", name, backLinks);
|
||||||
|
@ -11,220 +11,219 @@ import { PageMeta } from "./server";
|
|||||||
import { ClientPageState, Page } from "./types";
|
import { ClientPageState, Page } from "./types";
|
||||||
import { safeRun } from "./util";
|
import { safeRun } from "./util";
|
||||||
|
|
||||||
export class SocketAPI {
|
export function exposeSocketAPI(rootPath: string, io: Server) {
|
||||||
openPages = new Map<string, Page>();
|
const openPages = new Map<string, Page>();
|
||||||
connectedSockets: Set<Socket> = new Set();
|
const connectedSockets: Set<Socket> = new Set();
|
||||||
pageStore: DiskStorage;
|
const pageStore = new DiskStorage(rootPath);
|
||||||
|
fileWatcher(rootPath);
|
||||||
|
|
||||||
constructor(rootPath: string, io: Server) {
|
io.on("connection", (socket) => {
|
||||||
this.pageStore = new DiskStorage(rootPath);
|
const socketOpenPages = new Set<string>();
|
||||||
this.fileWatcher(rootPath);
|
|
||||||
|
|
||||||
io.on("connection", (socket) => {
|
console.log("Connected", socket.id);
|
||||||
console.log("Connected", socket.id);
|
connectedSockets.add(socket);
|
||||||
this.connectedSockets.add(socket);
|
|
||||||
const socketOpenPages = new Set<string>();
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
console.log("Disconnected", socket.id);
|
console.log("Disconnected", socket.id);
|
||||||
socketOpenPages.forEach(disconnectPageSocket);
|
socketOpenPages.forEach(disconnectPageSocket);
|
||||||
this.connectedSockets.delete(socket);
|
connectedSockets.delete(socket);
|
||||||
});
|
|
||||||
|
|
||||||
function onCall(eventName: string, cb: (...args: any[]) => Promise<any>) {
|
|
||||||
socket.on(eventName, (reqId: number, ...args) => {
|
|
||||||
cb(...args).then((result) => {
|
|
||||||
socket.emit(`${eventName}Resp${reqId}`, result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const _this = this;
|
|
||||||
function disconnectPageSocket(pageName: string) {
|
|
||||||
let page = _this.openPages.get(pageName);
|
|
||||||
if (page) {
|
|
||||||
for (let client of page.clientStates) {
|
|
||||||
if (client.socket === socket) {
|
|
||||||
_this.disconnectClient(client, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCall("openPage", async (pageName: string) => {
|
|
||||||
let page = this.openPages.get(pageName);
|
|
||||||
if (!page) {
|
|
||||||
try {
|
|
||||||
let { text, meta } = await this.pageStore.readPage(pageName);
|
|
||||||
page = new Page(pageName, text, meta);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Creating new page", pageName);
|
|
||||||
page = new Page(pageName, "", { name: pageName, lastModified: 0 });
|
|
||||||
}
|
|
||||||
this.openPages.set(pageName, page);
|
|
||||||
}
|
|
||||||
page.clientStates.add(new ClientPageState(socket, page.version));
|
|
||||||
socketOpenPages.add(pageName);
|
|
||||||
console.log("Opened page", pageName);
|
|
||||||
this.broadcastCursors(page);
|
|
||||||
return page.toJSON();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("closePage", (pageName: string) => {
|
|
||||||
console.log("Closing page", pageName);
|
|
||||||
socketOpenPages.delete(pageName);
|
|
||||||
disconnectPageSocket(pageName);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCall(
|
|
||||||
"pushUpdates",
|
|
||||||
async (
|
|
||||||
pageName: string,
|
|
||||||
version: number,
|
|
||||||
updates: any[]
|
|
||||||
): Promise<boolean> => {
|
|
||||||
let page = this.openPages.get(pageName);
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
console.error(
|
|
||||||
"Received updates for not open page",
|
|
||||||
pageName,
|
|
||||||
this.openPages.keys()
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (version !== page.version) {
|
|
||||||
console.error("Invalid version", version, page.version);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
console.log("Applying", updates.length, "updates to", pageName);
|
|
||||||
let transformedUpdates = [];
|
|
||||||
let textChanged = false;
|
|
||||||
for (let update of updates) {
|
|
||||||
let changes = ChangeSet.fromJSON(update.changes);
|
|
||||||
let transformedUpdate = {
|
|
||||||
changes,
|
|
||||||
clientID: update.clientID,
|
|
||||||
effects: update.cursors?.map((c: Cursor) => {
|
|
||||||
page.cursors.set(c.userId, c);
|
|
||||||
return cursorEffect.of(c);
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
page.updates.push(transformedUpdate);
|
|
||||||
transformedUpdates.push(transformedUpdate);
|
|
||||||
let oldText = page.text;
|
|
||||||
page.text = changes.apply(page.text);
|
|
||||||
if (oldText !== page.text) {
|
|
||||||
textChanged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textChanged) {
|
|
||||||
if (page.saveTimer) {
|
|
||||||
clearTimeout(page.saveTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
page.saveTimer = setTimeout(() => {
|
|
||||||
this.flushPageToDisk(pageName, page);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
while (page.pending.length) {
|
|
||||||
page.pending.pop()!(transformedUpdates);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onCall(
|
|
||||||
"pullUpdates",
|
|
||||||
async (pageName: string, version: number): Promise<Update[]> => {
|
|
||||||
let page = this.openPages.get(pageName);
|
|
||||||
// console.log("Pulling updates for", pageName);
|
|
||||||
if (!page) {
|
|
||||||
console.error("Fetching updates for not open page");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// TODO: Optimize this
|
|
||||||
let oldestVersion = Infinity;
|
|
||||||
page.clientStates.forEach((client) => {
|
|
||||||
oldestVersion = Math.min(client.version, oldestVersion);
|
|
||||||
if (client.socket === socket) {
|
|
||||||
client.version = version;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
page.flushUpdates(oldestVersion);
|
|
||||||
if (version < page.version) {
|
|
||||||
return page.updatesSince(version);
|
|
||||||
} else {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
page.pending.push(resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onCall(
|
|
||||||
"readPage",
|
|
||||||
async (pageName: string): Promise<{ text: string; meta: PageMeta }> => {
|
|
||||||
let page = this.openPages.get(pageName);
|
|
||||||
if (page) {
|
|
||||||
console.log("Serving page from memory", pageName);
|
|
||||||
return {
|
|
||||||
text: page.text.sliceString(0),
|
|
||||||
meta: page.meta,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return this.pageStore.readPage(pageName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onCall("writePage", async (pageName: string, text: string) => {
|
|
||||||
let page = this.openPages.get(pageName);
|
|
||||||
if (page) {
|
|
||||||
for (let client of page.clientStates) {
|
|
||||||
client.socket.emit("reloadPage", pageName);
|
|
||||||
}
|
|
||||||
this.openPages.delete(pageName);
|
|
||||||
}
|
|
||||||
return this.pageStore.writePage(pageName, text);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCall("deletePage", async (pageName: string) => {
|
|
||||||
this.openPages.delete(pageName);
|
|
||||||
socketOpenPages.delete(pageName);
|
|
||||||
// Cascading of this to all connected clients will be handled by file watcher
|
|
||||||
return this.pageStore.deletePage(pageName);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCall("listPages", async (): Promise<PageMeta[]> => {
|
|
||||||
return this.pageStore.listPages();
|
|
||||||
});
|
|
||||||
|
|
||||||
onCall("getPageMeta", async (pageName: string): Promise<PageMeta> => {
|
|
||||||
let page = this.openPages.get(pageName);
|
|
||||||
if (page) {
|
|
||||||
return page.meta;
|
|
||||||
}
|
|
||||||
return this.pageStore.getPageMeta(pageName);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
private disconnectClient(client: ClientPageState, page: Page) {
|
socket.on("closePage", (pageName: string) => {
|
||||||
|
console.log("Closing page", pageName);
|
||||||
|
socketOpenPages.delete(pageName);
|
||||||
|
disconnectPageSocket(pageName);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCall = (
|
||||||
|
eventName: string,
|
||||||
|
cb: (...args: any[]) => Promise<any>
|
||||||
|
) => {
|
||||||
|
socket.on(eventName, (reqId: number, ...args) => {
|
||||||
|
cb(...args).then((result) => {
|
||||||
|
socket.emit(`${eventName}Resp${reqId}`, result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnectPageSocket = (pageName: string) => {
|
||||||
|
let page = openPages.get(pageName);
|
||||||
|
if (page) {
|
||||||
|
for (let client of page.clientStates) {
|
||||||
|
if (client.socket === socket) {
|
||||||
|
disconnectClient(client, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onCall("openPage", async (pageName: string) => {
|
||||||
|
let page = openPages.get(pageName);
|
||||||
|
if (!page) {
|
||||||
|
try {
|
||||||
|
let { text, meta } = await pageStore.readPage(pageName);
|
||||||
|
page = new Page(pageName, text, meta);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Creating new page", pageName);
|
||||||
|
page = new Page(pageName, "", { name: pageName, lastModified: 0 });
|
||||||
|
}
|
||||||
|
openPages.set(pageName, page);
|
||||||
|
}
|
||||||
|
page.clientStates.add(new ClientPageState(socket, page.version));
|
||||||
|
socketOpenPages.add(pageName);
|
||||||
|
console.log("Opened page", pageName);
|
||||||
|
broadcastCursors(page);
|
||||||
|
return page.toJSON();
|
||||||
|
});
|
||||||
|
|
||||||
|
onCall(
|
||||||
|
"pushUpdates",
|
||||||
|
async (
|
||||||
|
pageName: string,
|
||||||
|
version: number,
|
||||||
|
updates: any[]
|
||||||
|
): Promise<boolean> => {
|
||||||
|
let page = openPages.get(pageName);
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error(
|
||||||
|
"Received updates for not open page",
|
||||||
|
pageName,
|
||||||
|
openPages.keys()
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (version !== page.version) {
|
||||||
|
console.error("Invalid version", version, page.version);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.log("Applying", updates.length, "updates to", pageName);
|
||||||
|
let transformedUpdates = [];
|
||||||
|
let textChanged = false;
|
||||||
|
for (let update of updates) {
|
||||||
|
let changes = ChangeSet.fromJSON(update.changes);
|
||||||
|
let transformedUpdate = {
|
||||||
|
changes,
|
||||||
|
clientID: update.clientID,
|
||||||
|
effects: update.cursors?.map((c: Cursor) => {
|
||||||
|
page.cursors.set(c.userId, c);
|
||||||
|
return cursorEffect.of(c);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
page.updates.push(transformedUpdate);
|
||||||
|
transformedUpdates.push(transformedUpdate);
|
||||||
|
let oldText = page.text;
|
||||||
|
page.text = changes.apply(page.text);
|
||||||
|
if (oldText !== page.text) {
|
||||||
|
textChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textChanged) {
|
||||||
|
if (page.saveTimer) {
|
||||||
|
clearTimeout(page.saveTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
page.saveTimer = setTimeout(() => {
|
||||||
|
flushPageToDisk(pageName, page);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
while (page.pending.length) {
|
||||||
|
page.pending.pop()!(transformedUpdates);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onCall(
|
||||||
|
"pullUpdates",
|
||||||
|
async (pageName: string, version: number): Promise<Update[]> => {
|
||||||
|
let page = openPages.get(pageName);
|
||||||
|
// console.log("Pulling updates for", pageName);
|
||||||
|
if (!page) {
|
||||||
|
console.error("Fetching updates for not open page");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// TODO: Optimize this
|
||||||
|
let oldestVersion = Infinity;
|
||||||
|
page.clientStates.forEach((client) => {
|
||||||
|
oldestVersion = Math.min(client.version, oldestVersion);
|
||||||
|
if (client.socket === socket) {
|
||||||
|
client.version = version;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
page.flushUpdates(oldestVersion);
|
||||||
|
if (version < page.version) {
|
||||||
|
return page.updatesSince(version);
|
||||||
|
} else {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
page.pending.push(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onCall(
|
||||||
|
"readPage",
|
||||||
|
async (pageName: string): Promise<{ text: string; meta: PageMeta }> => {
|
||||||
|
let page = openPages.get(pageName);
|
||||||
|
if (page) {
|
||||||
|
console.log("Serving page from memory", pageName);
|
||||||
|
return {
|
||||||
|
text: page.text.sliceString(0),
|
||||||
|
meta: page.meta,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return pageStore.readPage(pageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onCall("writePage", async (pageName: string, text: string) => {
|
||||||
|
let page = openPages.get(pageName);
|
||||||
|
if (page) {
|
||||||
|
for (let client of page.clientStates) {
|
||||||
|
client.socket.emit("reloadPage", pageName);
|
||||||
|
}
|
||||||
|
openPages.delete(pageName);
|
||||||
|
}
|
||||||
|
return pageStore.writePage(pageName, text);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCall("deletePage", async (pageName: string) => {
|
||||||
|
openPages.delete(pageName);
|
||||||
|
socketOpenPages.delete(pageName);
|
||||||
|
// Cascading of this to all connected clients will be handled by file watcher
|
||||||
|
return pageStore.deletePage(pageName);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCall("listPages", async (): Promise<PageMeta[]> => {
|
||||||
|
return pageStore.listPages();
|
||||||
|
});
|
||||||
|
|
||||||
|
onCall("getPageMeta", async (pageName: string): Promise<PageMeta> => {
|
||||||
|
let page = openPages.get(pageName);
|
||||||
|
if (page) {
|
||||||
|
return page.meta;
|
||||||
|
}
|
||||||
|
return pageStore.getPageMeta(pageName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function disconnectClient(client: ClientPageState, page: Page) {
|
||||||
page.clientStates.delete(client);
|
page.clientStates.delete(client);
|
||||||
if (page.clientStates.size === 0) {
|
if (page.clientStates.size === 0) {
|
||||||
console.log("No more clients for", page.name, "flushing");
|
console.log("No more clients for", page.name, "flushing");
|
||||||
this.flushPageToDisk(page.name, page);
|
flushPageToDisk(page.name, page);
|
||||||
this.openPages.delete(page.name);
|
openPages.delete(page.name);
|
||||||
} else {
|
} else {
|
||||||
page.cursors.delete(client.socket.id);
|
page.cursors.delete(client.socket.id);
|
||||||
this.broadcastCursors(page);
|
broadcastCursors(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private broadcastCursors(page: Page) {
|
function broadcastCursors(page: Page) {
|
||||||
page.clientStates.forEach((client) => {
|
page.clientStates.forEach((client) => {
|
||||||
client.socket.emit(
|
client.socket.emit(
|
||||||
"cursorSnapshot",
|
"cursorSnapshot",
|
||||||
@ -234,15 +233,15 @@ export class SocketAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private flushPageToDisk(name: string, page: Page) {
|
function flushPageToDisk(name: string, page: Page) {
|
||||||
safeRun(async () => {
|
safeRun(async () => {
|
||||||
let meta = await this.pageStore.writePage(name, page.text.sliceString(0));
|
let meta = await pageStore.writePage(name, page.text.sliceString(0));
|
||||||
console.log(`Wrote page ${name} to disk`);
|
console.log(`Wrote page ${name} to disk`);
|
||||||
page.meta = meta;
|
page.meta = meta;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private fileWatcher(rootPath: string) {
|
function fileWatcher(rootPath: string) {
|
||||||
fs.watch(
|
fs.watch(
|
||||||
rootPath,
|
rootPath,
|
||||||
{
|
{
|
||||||
@ -264,16 +263,16 @@ export class SocketAPI {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// File was deleted
|
// File was deleted
|
||||||
console.log("Deleted", pageName);
|
console.log("Deleted", pageName);
|
||||||
for (let socket of this.connectedSockets) {
|
for (let socket of connectedSockets) {
|
||||||
socket.emit("pageDeleted", pageName);
|
socket.emit("pageDeleted", pageName);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const openPage = this.openPages.get(pageName);
|
const openPage = openPages.get(pageName);
|
||||||
if (openPage) {
|
if (openPage) {
|
||||||
if (openPage.meta.lastModified < modifiedTime) {
|
if (openPage.meta.lastModified < modifiedTime) {
|
||||||
console.log("Page changed on disk outside of editor, reloading");
|
console.log("Page changed on disk outside of editor, reloading");
|
||||||
this.openPages.delete(pageName);
|
openPages.delete(pageName);
|
||||||
const meta = {
|
const meta = {
|
||||||
name: pageName,
|
name: pageName,
|
||||||
lastModified: modifiedTime,
|
lastModified: modifiedTime,
|
||||||
@ -288,7 +287,7 @@ export class SocketAPI {
|
|||||||
console.log(
|
console.log(
|
||||||
"New file created, broadcasting to all connected sockets"
|
"New file created, broadcasting to all connected sockets"
|
||||||
);
|
);
|
||||||
for (let socket of this.connectedSockets) {
|
for (let socket of connectedSockets) {
|
||||||
socket.emit("pageCreated", {
|
socket.emit("pageCreated", {
|
||||||
name: pageName,
|
name: pageName,
|
||||||
lastModified: modifiedTime,
|
lastModified: modifiedTime,
|
||||||
|
@ -12,9 +12,7 @@ export class DiskStorage {
|
|||||||
async listPages(): Promise<PageMeta[]> {
|
async listPages(): Promise<PageMeta[]> {
|
||||||
let fileNames: PageMeta[] = [];
|
let fileNames: PageMeta[] = [];
|
||||||
|
|
||||||
let _this = this;
|
const walkPath = async (dir: string) => {
|
||||||
|
|
||||||
async function walkPath(dir: string) {
|
|
||||||
let files = await readdir(dir);
|
let files = await readdir(dir);
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
const fullPath = path.join(dir, file);
|
const fullPath = path.join(dir, file);
|
||||||
@ -25,7 +23,7 @@ export class DiskStorage {
|
|||||||
if (path.extname(file) === ".md") {
|
if (path.extname(file) === ".md") {
|
||||||
fileNames.push({
|
fileNames.push({
|
||||||
name: fullPath.substring(
|
name: fullPath.substring(
|
||||||
_this.rootPath.length + 1,
|
this.rootPath.length + 1,
|
||||||
fullPath.length - 3
|
fullPath.length - 3
|
||||||
),
|
),
|
||||||
lastModified: s.mtime.getTime(),
|
lastModified: s.mtime.getTime(),
|
||||||
@ -33,7 +31,7 @@ export class DiskStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
await walkPath(this.rootPath);
|
await walkPath(this.rootPath);
|
||||||
return fileNames;
|
return fileNames;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import express from "express";
|
|||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
import http from "http";
|
import http from "http";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import { SocketAPI } from "./api";
|
import { exposeSocketAPI } from "./api";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
@ -24,7 +24,7 @@ export type PageMeta = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
app.use("/", express.static(distDir));
|
app.use("/", express.static(distDir));
|
||||||
let filesystem = new SocketAPI(pagesPath, io);
|
exposeSocketAPI(pagesPath, io);
|
||||||
|
|
||||||
// Fallback, serve index.html
|
// Fallback, serve index.html
|
||||||
let cachedIndex: string | undefined = undefined;
|
let cachedIndex: string | undefined = undefined;
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"@parcel/transformer-webmanifest": "2.3.2",
|
"@parcel/transformer-webmanifest": "2.3.2",
|
||||||
"@parcel/validator-typescript": "^2.3.2",
|
"@parcel/validator-typescript": "^2.3.2",
|
||||||
"@types/events": "^3.0.0",
|
"@types/events": "^3.0.0",
|
||||||
"@types/lodash": "^4.14.179",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "^17.0.39",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
// TODO:
|
|
||||||
// Send state to client
|
|
||||||
// Shape of editor.editorView.state.toJSON({"cursors": cursorField})
|
|
||||||
// From there import it
|
|
||||||
// EditorState.fromJSON(js, {extensions: cursorField}, {cursors: cursorField})
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
collab,
|
collab,
|
||||||
getSyncedVersion,
|
getSyncedVersion,
|
||||||
receiveUpdates,
|
receiveUpdates,
|
||||||
sendableUpdates,
|
sendableUpdates,
|
||||||
|
Update,
|
||||||
} from "@codemirror/collab";
|
} from "@codemirror/collab";
|
||||||
import { RangeSetBuilder } from "@codemirror/rangeset";
|
import { RangeSetBuilder } from "@codemirror/rangeset";
|
||||||
import { Text } from "@codemirror/state";
|
import { Text, Transaction } from "@codemirror/state";
|
||||||
import {
|
import {
|
||||||
Decoration,
|
Decoration,
|
||||||
DecorationSet,
|
DecorationSet,
|
||||||
@ -20,27 +15,13 @@ import {
|
|||||||
ViewUpdate,
|
ViewUpdate,
|
||||||
WidgetType,
|
WidgetType,
|
||||||
} from "@codemirror/view";
|
} from "@codemirror/view";
|
||||||
|
import { throttle } from "./util";
|
||||||
import { Cursor, cursorEffect } from "./cursorEffect";
|
import { Cursor, cursorEffect } from "./cursorEffect";
|
||||||
import { RealtimeSpace, SpaceEventHandlers } from "./space";
|
import { EventEmitter } from "./event";
|
||||||
|
|
||||||
const throttleInterval = 250;
|
const throttleInterval = 250;
|
||||||
|
|
||||||
const throttle = (func: () => void, limit: number) => {
|
export class CollabDocument {
|
||||||
let timer: any = null;
|
|
||||||
return function () {
|
|
||||||
if (!timer) {
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
func();
|
|
||||||
timer = null;
|
|
||||||
}, limit);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
window.throttle = throttle;
|
|
||||||
|
|
||||||
export class Document {
|
|
||||||
text: Text;
|
text: Text;
|
||||||
version: number;
|
version: number;
|
||||||
cursors: Map<string, Cursor>;
|
cursors: Map<string, Cursor>;
|
||||||
@ -81,24 +62,39 @@ class CursorWidget extends WidgetType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CollabEvents = {
|
||||||
|
cursorSnapshot: (pageName: string, cursors: Map<string, Cursor>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export function collabExtension(
|
export function collabExtension(
|
||||||
pageName: string,
|
pageName: string,
|
||||||
clientID: string,
|
clientID: string,
|
||||||
doc: Document,
|
doc: CollabDocument,
|
||||||
space: RealtimeSpace,
|
collabEmitter: EventEmitter<CollabEvents>,
|
||||||
reloadCallback: () => void
|
callbacks: {
|
||||||
|
pushUpdates: (
|
||||||
|
pageName: string,
|
||||||
|
version: number,
|
||||||
|
updates: readonly (Update & { origin: Transaction })[]
|
||||||
|
) => Promise<boolean>;
|
||||||
|
pullUpdates: (
|
||||||
|
pageName: string,
|
||||||
|
version: number
|
||||||
|
) => Promise<readonly Update[]>;
|
||||||
|
reload: () => void;
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
let plugin = ViewPlugin.fromClass(
|
let plugin = ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
private pushing = false;
|
private pushing = false;
|
||||||
private done = false;
|
private done = false;
|
||||||
private failedPushes = 0;
|
private failedPushes = 0;
|
||||||
decorations: DecorationSet;
|
|
||||||
private cursorPositions: Map<string, Cursor> = doc.cursors;
|
private cursorPositions: Map<string, Cursor> = doc.cursors;
|
||||||
|
decorations: DecorationSet;
|
||||||
|
|
||||||
throttledPush = throttle(() => this.push(), throttleInterval);
|
throttledPush = throttle(() => this.push(), throttleInterval);
|
||||||
|
|
||||||
eventHandlers: Partial<SpaceEventHandlers> = {
|
eventHandlers: Partial<CollabEvents> = {
|
||||||
cursorSnapshot: (pageName, cursors) => {
|
cursorSnapshot: (pageName, cursors) => {
|
||||||
console.log("Received new cursor snapshot", cursors);
|
console.log("Received new cursor snapshot", cursors);
|
||||||
this.cursorPositions = new Map(Object.entries(cursors));
|
this.cursorPositions = new Map(Object.entries(cursors));
|
||||||
@ -136,7 +132,7 @@ export function collabExtension(
|
|||||||
this.pull();
|
this.pull();
|
||||||
}
|
}
|
||||||
this.decorations = this.buildDecorations(view);
|
this.decorations = this.buildDecorations(view);
|
||||||
space.on(this.eventHandlers);
|
collabEmitter.on(this.eventHandlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
@ -184,7 +180,7 @@ export function collabExtension(
|
|||||||
console.log("Updates", updates);
|
console.log("Updates", updates);
|
||||||
this.pushing = true;
|
this.pushing = true;
|
||||||
let version = getSyncedVersion(this.view.state);
|
let version = getSyncedVersion(this.view.state);
|
||||||
let success = await space.pushUpdates(pageName, version, updates);
|
let success = await callbacks.pushUpdates(pageName, version, updates);
|
||||||
this.pushing = false;
|
this.pushing = false;
|
||||||
|
|
||||||
if (!success && !this.done) {
|
if (!success && !this.done) {
|
||||||
@ -192,7 +188,7 @@ export function collabExtension(
|
|||||||
if (this.failedPushes > 10) {
|
if (this.failedPushes > 10) {
|
||||||
// Not sure if 10 is a good number, but YOLO
|
// Not sure if 10 is a good number, but YOLO
|
||||||
console.log("10 pushes failed, reloading");
|
console.log("10 pushes failed, reloading");
|
||||||
reloadCallback();
|
callbacks.reload();
|
||||||
return this.destroy();
|
return this.destroy();
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
@ -213,7 +209,7 @@ export function collabExtension(
|
|||||||
async pull() {
|
async pull() {
|
||||||
while (!this.done) {
|
while (!this.done) {
|
||||||
let version = getSyncedVersion(this.view.state);
|
let version = getSyncedVersion(this.view.state);
|
||||||
let updates = await space.pullUpdates(pageName, version);
|
let updates = await callbacks.pullUpdates(pageName, version);
|
||||||
let d = receiveUpdates(this.view.state, updates);
|
let d = receiveUpdates(this.view.state, updates);
|
||||||
// Pull out cursor updates and update local state
|
// Pull out cursor updates and update local state
|
||||||
for (let update of updates) {
|
for (let update of updates) {
|
||||||
@ -235,7 +231,7 @@ export function collabExtension(
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.done = true;
|
this.done = true;
|
||||||
space.off(this.eventHandlers);
|
collabEmitter.off(this.eventHandlers);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { PageMeta } from "../types";
|
import { AppViewState, PageMeta } from "../types";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faFileLines } from "@fortawesome/free-solid-svg-icons";
|
import { faFileLines } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { Notification } from "../types";
|
||||||
|
|
||||||
function prettyName(s: string | undefined): string {
|
function prettyName(s: string | undefined): string {
|
||||||
if (!s) {
|
if (!s) {
|
||||||
@ -10,10 +11,14 @@ function prettyName(s: string | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TopBar({
|
export function TopBar({
|
||||||
currentPage,
|
pageName,
|
||||||
|
status,
|
||||||
|
notifications,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
currentPage?: string;
|
pageName?: string;
|
||||||
|
status?: string;
|
||||||
|
notifications: Notification[];
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -22,7 +27,12 @@ export function TopBar({
|
|||||||
<span className="icon">
|
<span className="icon">
|
||||||
<FontAwesomeIcon icon={faFileLines} />
|
<FontAwesomeIcon icon={faFileLines} />
|
||||||
</span>
|
</span>
|
||||||
<span className="current-page">{prettyName(currentPage)}</span>
|
<span className="current-page">{prettyName(pageName)}</span>
|
||||||
|
<div className="status">
|
||||||
|
{notifications.map((notification) => (
|
||||||
|
<div key={notification.id}>{notification.message}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -10,7 +10,13 @@ import { indentWithTab, standardKeymap } from "@codemirror/commands";
|
|||||||
import { history, historyKeymap } from "@codemirror/history";
|
import { history, historyKeymap } from "@codemirror/history";
|
||||||
import { bracketMatching } from "@codemirror/matchbrackets";
|
import { bracketMatching } from "@codemirror/matchbrackets";
|
||||||
import { searchKeymap } from "@codemirror/search";
|
import { searchKeymap } from "@codemirror/search";
|
||||||
import { EditorState, StateField, Transaction, Text } from "@codemirror/state";
|
import {
|
||||||
|
EditorSelection,
|
||||||
|
EditorState,
|
||||||
|
StateField,
|
||||||
|
Text,
|
||||||
|
Transaction,
|
||||||
|
} from "@codemirror/state";
|
||||||
import {
|
import {
|
||||||
drawSelection,
|
drawSelection,
|
||||||
dropCursor,
|
dropCursor,
|
||||||
@ -19,30 +25,25 @@ import {
|
|||||||
KeyBinding,
|
KeyBinding,
|
||||||
keymap,
|
keymap,
|
||||||
} from "@codemirror/view";
|
} from "@codemirror/view";
|
||||||
|
// import { debounce } from "lodash";
|
||||||
import { debounce } from "lodash";
|
|
||||||
|
|
||||||
import React, { useEffect, useReducer } from "react";
|
import React, { useEffect, useReducer } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import coreManifest from "./generated/core.plug.json";
|
import { Plug, System } from "../../plugbox/src/runtime";
|
||||||
|
import { WebworkerSandbox } from "../../plugbox/src/worker_sandbox";
|
||||||
// @ts-ignore
|
|
||||||
window.coreManifest = coreManifest;
|
|
||||||
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
|
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
|
||||||
|
import { collabExtension, CollabDocument } from "./collab";
|
||||||
import * as commands from "./commands";
|
import * as commands from "./commands";
|
||||||
import { CommandPalette } from "./components/command_palette";
|
import { CommandPalette } from "./components/command_palette";
|
||||||
import { PageNavigator } from "./components/page_navigator";
|
import { PageNavigator } from "./components/page_navigator";
|
||||||
import { StatusBar } from "./components/status_bar";
|
import { StatusBar } from "./components/status_bar";
|
||||||
import { TopBar } from "./components/top_bar";
|
import { TopBar } from "./components/top_bar";
|
||||||
|
import { Cursor } from "./cursorEffect";
|
||||||
|
import coreManifest from "./generated/core.plug.json";
|
||||||
import { Indexer } from "./indexer";
|
import { Indexer } from "./indexer";
|
||||||
import { lineWrapper } from "./lineWrapper";
|
import { lineWrapper } from "./lineWrapper";
|
||||||
import { markdown } from "./markdown";
|
import { markdown } from "./markdown";
|
||||||
import { IPageNavigator, PathPageNavigator } from "./navigator";
|
import { IPageNavigator, PathPageNavigator } from "./navigator";
|
||||||
import customMarkDown from "./parser";
|
import customMarkDown from "./parser";
|
||||||
import { System } from "../../plugbox/src/runtime";
|
|
||||||
import { Plug } from "../../plugbox/src/runtime";
|
|
||||||
import { slashCommandRegexp } from "./types";
|
|
||||||
|
|
||||||
import reducer from "./reducer";
|
import reducer from "./reducer";
|
||||||
import { smartQuoteKeymap } from "./smart_quotes";
|
import { smartQuoteKeymap } from "./smart_quotes";
|
||||||
import { RealtimeSpace } from "./space";
|
import { RealtimeSpace } from "./space";
|
||||||
@ -57,15 +58,9 @@ import {
|
|||||||
AppViewState,
|
AppViewState,
|
||||||
initialViewState,
|
initialViewState,
|
||||||
NuggetHook,
|
NuggetHook,
|
||||||
PageMeta,
|
slashCommandRegexp,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { safeRun } from "./util";
|
import { safeRun, throttle } from "./util";
|
||||||
|
|
||||||
import { collabExtension } from "./collab";
|
|
||||||
|
|
||||||
import { Document } from "./collab";
|
|
||||||
import { EditorSelection } from "@codemirror/state";
|
|
||||||
import { Cursor } from "./cursorEffect";
|
|
||||||
|
|
||||||
class PageState {
|
class PageState {
|
||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
@ -77,8 +72,6 @@ class PageState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const watchInterval = 5000;
|
|
||||||
|
|
||||||
export class Editor implements AppEventDispatcher {
|
export class Editor implements AppEventDispatcher {
|
||||||
editorView?: EditorView;
|
editorView?: EditorView;
|
||||||
viewState: AppViewState;
|
viewState: AppViewState;
|
||||||
@ -103,18 +96,20 @@ export class Editor implements AppEventDispatcher {
|
|||||||
this.editorView = new EditorView({
|
this.editorView = new EditorView({
|
||||||
state: this.createEditorState(
|
state: this.createEditorState(
|
||||||
"",
|
"",
|
||||||
new Document(Text.of([""]), 0, new Map<string, Cursor>())
|
new CollabDocument(Text.of([""]), 0, new Map<string, Cursor>())
|
||||||
),
|
),
|
||||||
parent: document.getElementById("editor")!,
|
parent: document.getElementById("editor")!,
|
||||||
});
|
});
|
||||||
this.pageNavigator = new PathPageNavigator();
|
this.pageNavigator = new PathPageNavigator();
|
||||||
this.indexer = new Indexer("page-index", space);
|
this.indexer = new Indexer("page-index", space);
|
||||||
|
|
||||||
this.indexCurrentPageDebounced = debounce(this.indexCurrentPage, 2000);
|
this.indexCurrentPageDebounced = throttle(
|
||||||
|
this.indexCurrentPage.bind(this),
|
||||||
|
2000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// await this.loadPageList();
|
|
||||||
await this.loadPlugs();
|
await this.loadPlugs();
|
||||||
this.focus();
|
this.focus();
|
||||||
|
|
||||||
@ -125,16 +120,6 @@ export class Editor implements AppEventDispatcher {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentPage) {
|
|
||||||
let pageState = this.openPages.get(this.currentPage)!;
|
|
||||||
if (pageState) {
|
|
||||||
pageState.selection = this.editorView!.state.selection;
|
|
||||||
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.space.closePage(this.currentPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.loadPage(pageName);
|
await this.loadPage(pageName);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -142,12 +127,14 @@ export class Editor implements AppEventDispatcher {
|
|||||||
connect: () => {
|
connect: () => {
|
||||||
if (this.currentPage) {
|
if (this.currentPage) {
|
||||||
console.log("Connected to socket, fetch fresh?");
|
console.log("Connected to socket, fetch fresh?");
|
||||||
|
this.flashNotification("Reconnected, reloading page");
|
||||||
this.reloadPage();
|
this.reloadPage();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pageChanged: (meta) => {
|
pageChanged: (meta) => {
|
||||||
if (this.currentPage === meta.name) {
|
if (this.currentPage === meta.name) {
|
||||||
console.log("page changed on disk, reloading");
|
console.log("Page changed on disk, reloading");
|
||||||
|
this.flashNotification("Page changed on disk, reloading");
|
||||||
this.reloadPage();
|
this.reloadPage();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -164,6 +151,24 @@ export class Editor implements AppEventDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flashNotification(message: string) {
|
||||||
|
let id = Math.floor(Math.random() * 1000000);
|
||||||
|
this.viewDispatch({
|
||||||
|
type: "show-notification",
|
||||||
|
notification: {
|
||||||
|
id: id,
|
||||||
|
message: message,
|
||||||
|
date: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
this.viewDispatch({
|
||||||
|
type: "dismiss-notification",
|
||||||
|
id: id,
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
async loadPlugs() {
|
async loadPlugs() {
|
||||||
const system = new System<NuggetHook>();
|
const system = new System<NuggetHook>();
|
||||||
system.registerSyscalls(
|
system.registerSyscalls(
|
||||||
@ -174,7 +179,11 @@ export class Editor implements AppEventDispatcher {
|
|||||||
);
|
);
|
||||||
|
|
||||||
console.log("Now loading core plug");
|
console.log("Now loading core plug");
|
||||||
let mainPlug = await system.load("core", coreManifest);
|
let mainPlug = await system.load(
|
||||||
|
"core",
|
||||||
|
coreManifest,
|
||||||
|
new WebworkerSandbox(system)
|
||||||
|
);
|
||||||
this.plugs.push(mainPlug);
|
this.plugs.push(mainPlug);
|
||||||
this.editorCommands = new Map<string, AppCommand>();
|
this.editorCommands = new Map<string, AppCommand>();
|
||||||
for (let plug of this.plugs) {
|
for (let plug of this.plugs) {
|
||||||
@ -217,7 +226,7 @@ export class Editor implements AppEventDispatcher {
|
|||||||
return this.viewState.currentPage;
|
return this.viewState.currentPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
createEditorState(pageName: string, doc: Document): EditorState {
|
createEditorState(pageName: string, doc: CollabDocument): EditorState {
|
||||||
const editor = this;
|
const editor = this;
|
||||||
let commandKeyBindings: KeyBinding[] = [];
|
let commandKeyBindings: KeyBinding[] = [];
|
||||||
for (let def of this.editorCommands.values()) {
|
for (let def of this.editorCommands.values()) {
|
||||||
@ -243,17 +252,14 @@ export class Editor implements AppEventDispatcher {
|
|||||||
history(),
|
history(),
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
// indentOnInput(),
|
|
||||||
customMarkdownStyle,
|
customMarkdownStyle,
|
||||||
bracketMatching(),
|
bracketMatching(),
|
||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
collabExtension(
|
collabExtension(pageName, this.space.socket.id, doc, this.space, {
|
||||||
pageName,
|
pushUpdates: this.space.pushUpdates.bind(this.space),
|
||||||
this.space.socket.id,
|
pullUpdates: this.space.pullUpdates.bind(this.space),
|
||||||
doc,
|
reload: this.reloadPage.bind(this),
|
||||||
this.space,
|
}),
|
||||||
this.reloadPage.bind(this)
|
|
||||||
),
|
|
||||||
autocompletion({
|
autocompletion({
|
||||||
override: [
|
override: [
|
||||||
this.plugCompleter.bind(this),
|
this.plugCompleter.bind(this),
|
||||||
@ -428,13 +434,26 @@ export class Editor implements AppEventDispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadPage(pageName: string) {
|
async loadPage(pageName: string) {
|
||||||
let doc = await this.space.openPage(pageName);
|
|
||||||
let editorState = this.createEditorState(pageName, doc);
|
|
||||||
let pageState = this.openPages.get(pageName);
|
|
||||||
const editorView = this.editorView;
|
const editorView = this.editorView;
|
||||||
if (!editorView) {
|
if (!editorView) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist current page state and nicely close page
|
||||||
|
if (this.currentPage) {
|
||||||
|
let pageState = this.openPages.get(this.currentPage)!;
|
||||||
|
if (pageState) {
|
||||||
|
pageState.selection = this.editorView!.state.selection;
|
||||||
|
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.space.closePage(this.currentPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch next page to open
|
||||||
|
let doc = await this.space.openPage(pageName);
|
||||||
|
let editorState = this.createEditorState(pageName, doc);
|
||||||
|
let pageState = this.openPages.get(pageName);
|
||||||
editorView.setState(editorState);
|
editorView.setState(editorState);
|
||||||
if (!pageState) {
|
if (!pageState) {
|
||||||
pageState = new PageState(0, editorState.selection);
|
pageState = new PageState(0, editorState.selection);
|
||||||
@ -444,7 +463,7 @@ export class Editor implements AppEventDispatcher {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Restore state
|
// Restore state
|
||||||
console.log("Restoring selection state");
|
console.log("Restoring selection state", pageState.selection);
|
||||||
editorView.dispatch({
|
editorView.dispatch({
|
||||||
selection: pageState.selection,
|
selection: pageState.selection,
|
||||||
});
|
});
|
||||||
@ -456,15 +475,8 @@ export class Editor implements AppEventDispatcher {
|
|||||||
name: pageName,
|
name: pageName,
|
||||||
});
|
});
|
||||||
|
|
||||||
// let indexerPageMeta = await this.indexer.getPageIndexPageMeta(pageName);
|
// TODO: Check if indexing is required?
|
||||||
// if (
|
|
||||||
// (indexerPageMeta &&
|
|
||||||
// doc.meta.lastModified.getTime() !==
|
|
||||||
// indexerPageMeta.lastModified.getTime()) ||
|
|
||||||
// !indexerPageMeta
|
|
||||||
// ) {
|
|
||||||
await this.indexCurrentPage();
|
await this.indexCurrentPage();
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewComponent(): React.ReactElement {
|
ViewComponent(): React.ReactElement {
|
||||||
@ -513,13 +525,13 @@ export class Editor implements AppEventDispatcher {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TopBar
|
<TopBar
|
||||||
currentPage={viewState.currentPage}
|
pageName={viewState.currentPage}
|
||||||
|
notifications={viewState.notifications}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({ type: "start-navigate" });
|
dispatch({ type: "start-navigate" });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div id="editor"></div>
|
<div id="editor"></div>
|
||||||
<StatusBar editorView={this.editorView} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
20
webapp/src/event.ts
Normal file
20
webapp/src/event.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export abstract class EventEmitter<HandlerT> {
|
||||||
|
private handlers: Partial<HandlerT>[] = [];
|
||||||
|
|
||||||
|
on(handlers: Partial<HandlerT>) {
|
||||||
|
this.handlers.push(handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(handlers: Partial<HandlerT>) {
|
||||||
|
this.handlers = this.handlers.filter((h) => h !== handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(eventName: keyof HandlerT, ...args: any[]) {
|
||||||
|
for (let handler of this.handlers) {
|
||||||
|
let fn: any = handler[eventName];
|
||||||
|
if (fn) {
|
||||||
|
fn(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -48,6 +48,16 @@ export default function reducer(
|
|||||||
...state,
|
...state,
|
||||||
commands: action.commands,
|
commands: action.commands,
|
||||||
};
|
};
|
||||||
|
case "show-notification":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
notifications: [action.notification, ...state.notifications],
|
||||||
|
};
|
||||||
|
case "dismiss-notification":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
notifications: state.notifications.filter((n) => n.id !== action.id),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,9 @@ import { Socket } from "socket.io-client";
|
|||||||
import { Update } from "@codemirror/collab";
|
import { Update } from "@codemirror/collab";
|
||||||
import { Transaction, Text, ChangeSet } from "@codemirror/state";
|
import { Transaction, Text, ChangeSet } from "@codemirror/state";
|
||||||
|
|
||||||
import { Document } from "./collab";
|
import { CollabEvents, CollabDocument } from "./collab";
|
||||||
import { Cursor, cursorEffect } from "./cursorEffect";
|
import { Cursor, cursorEffect } from "./cursorEffect";
|
||||||
|
import { EventEmitter } from "./event";
|
||||||
|
|
||||||
export interface Space {
|
export interface Space {
|
||||||
listPages(): Promise<PageMeta[]>;
|
listPages(): Promise<PageMeta[]>;
|
||||||
@ -14,43 +15,15 @@ export interface Space {
|
|||||||
getPageMeta(name: string): Promise<PageMeta>;
|
getPageMeta(name: string): Promise<PageMeta>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SpaceEventHandlers = {
|
export type SpaceEvents = {
|
||||||
connect: () => void;
|
connect: () => void;
|
||||||
cursorSnapshot: (
|
|
||||||
pageName: string,
|
|
||||||
cursors: { [key: string]: Cursor }
|
|
||||||
) => void;
|
|
||||||
pageCreated: (meta: PageMeta) => void;
|
pageCreated: (meta: PageMeta) => void;
|
||||||
pageChanged: (meta: PageMeta) => void;
|
pageChanged: (meta: PageMeta) => void;
|
||||||
pageDeleted: (name: string) => void;
|
pageDeleted: (name: string) => void;
|
||||||
pageListUpdated: (pages: Set<PageMeta>) => void;
|
pageListUpdated: (pages: Set<PageMeta>) => void;
|
||||||
};
|
} & CollabEvents;
|
||||||
|
|
||||||
abstract class EventEmitter<HandlerT> {
|
export class RealtimeSpace extends EventEmitter<SpaceEvents> implements Space {
|
||||||
private handlers: Partial<HandlerT>[] = [];
|
|
||||||
|
|
||||||
on(handlers: Partial<HandlerT>) {
|
|
||||||
this.handlers.push(handlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
off(handlers: Partial<HandlerT>) {
|
|
||||||
this.handlers = this.handlers.filter((h) => h !== handlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(eventName: keyof HandlerT, ...args: any[]) {
|
|
||||||
for (let handler of this.handlers) {
|
|
||||||
let fn: any = handler[eventName];
|
|
||||||
if (fn) {
|
|
||||||
fn(...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RealtimeSpace
|
|
||||||
extends EventEmitter<SpaceEventHandlers>
|
|
||||||
implements Space
|
|
||||||
{
|
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
reqId = 0;
|
reqId = 0;
|
||||||
allPages = new Set<PageMeta>();
|
allPages = new Set<PageMeta>();
|
||||||
@ -67,7 +40,7 @@ export class RealtimeSpace
|
|||||||
"pageDeleted",
|
"pageDeleted",
|
||||||
].forEach((eventName) => {
|
].forEach((eventName) => {
|
||||||
socket.on(eventName, (...args) => {
|
socket.on(eventName, (...args) => {
|
||||||
this.emit(eventName as keyof SpaceEventHandlers, ...args);
|
this.emit(eventName as keyof SpaceEvents, ...args);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.wsCall("listPages").then((pages) => {
|
this.wsCall("listPages").then((pages) => {
|
||||||
@ -133,18 +106,19 @@ export class RealtimeSpace
|
|||||||
return Array.from(this.allPages);
|
return Array.from(this.allPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
async openPage(name: string): Promise<Document> {
|
async openPage(name: string): Promise<CollabDocument> {
|
||||||
this.reqId++;
|
this.reqId++;
|
||||||
let pageJSON = await this.wsCall("openPage", name);
|
let pageJSON = await this.wsCall("openPage", name);
|
||||||
let cursors = new Map<string, Cursor>();
|
|
||||||
for (let p in pageJSON.cursors) {
|
return new CollabDocument(
|
||||||
cursors.set(p, pageJSON.cursors[p]);
|
Text.of(pageJSON.text),
|
||||||
}
|
pageJSON.version,
|
||||||
return new Document(Text.of(pageJSON.text), pageJSON.version, cursors);
|
new Map(Object.entries(pageJSON.cursors))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async closePage(name: string): Promise<void> {
|
async closePage(name: string): Promise<void> {
|
||||||
this.socket!.emit("closePage", name);
|
this.socket.emit("closePage", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
||||||
|
@ -32,34 +32,41 @@ body {
|
|||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
|
||||||
|
|
||||||
.current-page {
|
.status {
|
||||||
font-family: var(--ui-font);
|
float: right;
|
||||||
font-weight: bold;
|
border: rgb(41, 41, 41) 1px solid;
|
||||||
}
|
border-radius: 5px;
|
||||||
|
padding: 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.current-page {
|
||||||
|
font-family: var(--ui-font);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#bottom {
|
// #bottom {
|
||||||
position: fixed;
|
// position: fixed;
|
||||||
bottom: 0;
|
// bottom: 0;
|
||||||
left: 0;
|
// left: 0;
|
||||||
right: 0;
|
// right: 0;
|
||||||
height: 20px;
|
// height: 20px;
|
||||||
background-color: rgb(232, 232, 232);
|
// background-color: rgb(232, 232, 232);
|
||||||
color: rgb(79, 78, 78);
|
// color: rgb(79, 78, 78);
|
||||||
border-top: rgb(186, 186, 186) 1px solid;
|
// border-top: rgb(186, 186, 186) 1px solid;
|
||||||
margin: 0;
|
// margin: 0;
|
||||||
padding: 5px 10px;
|
// padding: 5px 10px;
|
||||||
font-family: var(--ui-font);
|
// font-family: var(--ui-font);
|
||||||
font-size: 0.9em;
|
// font-size: 0.9em;
|
||||||
text-align: right;
|
// text-align: right;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// body.keyboard #bottom {
|
// body.keyboard #bottom {
|
||||||
// bottom: 250px;
|
// bottom: 250px;
|
||||||
@ -68,7 +75,7 @@ body {
|
|||||||
#editor {
|
#editor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 55px;
|
top: 55px;
|
||||||
bottom: 30px;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
@ -35,12 +35,19 @@ export interface CommandDef {
|
|||||||
slashCommand?: string;
|
slashCommand?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Notification = {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
date: Date;
|
||||||
|
};
|
||||||
|
|
||||||
export type AppViewState = {
|
export type AppViewState = {
|
||||||
currentPage?: string;
|
currentPage?: string;
|
||||||
showPageNavigator: boolean;
|
showPageNavigator: boolean;
|
||||||
showCommandPalette: boolean;
|
showCommandPalette: boolean;
|
||||||
allPages: Set<PageMeta>;
|
allPages: Set<PageMeta>;
|
||||||
commands: Map<string, AppCommand>;
|
commands: Map<string, AppCommand>;
|
||||||
|
notifications: Notification[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialViewState: AppViewState = {
|
export const initialViewState: AppViewState = {
|
||||||
@ -48,6 +55,7 @@ export const initialViewState: AppViewState = {
|
|||||||
showCommandPalette: false,
|
showCommandPalette: false,
|
||||||
allPages: new Set(),
|
allPages: new Set(),
|
||||||
commands: new Map(),
|
commands: new Map(),
|
||||||
|
notifications: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
@ -57,4 +65,6 @@ export type Action =
|
|||||||
| { type: "stop-navigate" }
|
| { type: "stop-navigate" }
|
||||||
| { type: "update-commands"; commands: Map<string, AppCommand> }
|
| { type: "update-commands"; commands: Map<string, AppCommand> }
|
||||||
| { type: "show-palette" }
|
| { type: "show-palette" }
|
||||||
| { type: "hide-palette" };
|
| { type: "hide-palette" }
|
||||||
|
| { type: "show-notification"; notification: Notification }
|
||||||
|
| { type: "dismiss-notification"; id: number };
|
||||||
|
@ -17,3 +17,15 @@ export function safeRun(fn: () => Promise<void>) {
|
|||||||
export function isMacLike() {
|
export function isMacLike() {
|
||||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function throttle(func: () => void, limit: number) {
|
||||||
|
let timer: any = null;
|
||||||
|
return function () {
|
||||||
|
if (!timer) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
func();
|
||||||
|
timer = null;
|
||||||
|
}, limit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -979,10 +979,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||||
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
||||||
|
|
||||||
"@types/lodash@^4.14.179":
|
"@types/jest@^27.4.1":
|
||||||
version "4.14.179"
|
version "27.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5"
|
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d"
|
||||||
integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==
|
integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==
|
||||||
|
dependencies:
|
||||||
|
jest-matcher-utils "^27.0.0"
|
||||||
|
pretty-format "^27.0.0"
|
||||||
|
|
||||||
"@types/node@^17.0.21":
|
"@types/node@^17.0.21":
|
||||||
version "17.0.21"
|
version "17.0.21"
|
||||||
@ -1025,6 +1028,11 @@ abortcontroller-polyfill@^1.1.9:
|
|||||||
resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5"
|
resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5"
|
||||||
integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==
|
integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==
|
||||||
|
|
||||||
|
ansi-regex@^5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||||
|
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||||
|
|
||||||
ansi-styles@^3.2.1:
|
ansi-styles@^3.2.1:
|
||||||
version "3.2.1"
|
version "3.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||||
@ -1039,6 +1047,11 @@ ansi-styles@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert "^2.0.1"
|
color-convert "^2.0.1"
|
||||||
|
|
||||||
|
ansi-styles@^5.0.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
|
||||||
|
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||||
|
|
||||||
anymatch@~3.1.2:
|
anymatch@~3.1.2:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
|
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
|
||||||
@ -1241,7 +1254,7 @@ chalk@^2.0.0:
|
|||||||
escape-string-regexp "^1.0.5"
|
escape-string-regexp "^1.0.5"
|
||||||
supports-color "^5.3.0"
|
supports-color "^5.3.0"
|
||||||
|
|
||||||
chalk@^4.1.0:
|
chalk@^4.0.0, chalk@^4.1.0:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||||
@ -1514,6 +1527,11 @@ dexie@^3.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.1.tgz#ef21456d725e700c1ab7ac4307896e4fdabaf753"
|
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.1.tgz#ef21456d725e700c1ab7ac4307896e4fdabaf753"
|
||||||
integrity sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g==
|
integrity sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g==
|
||||||
|
|
||||||
|
diff-sequences@^27.5.1:
|
||||||
|
version "27.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
|
||||||
|
integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
|
||||||
|
|
||||||
diffie-hellman@^5.0.0:
|
diffie-hellman@^5.0.0:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||||
@ -2003,6 +2021,31 @@ is-weakref@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
call-bind "^1.0.2"
|
call-bind "^1.0.2"
|
||||||
|
|
||||||
|
jest-diff@^27.5.1:
|
||||||
|
version "27.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
|
||||||
|
integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
|
||||||
|
dependencies:
|
||||||
|
chalk "^4.0.0"
|
||||||
|
diff-sequences "^27.5.1"
|
||||||
|
jest-get-type "^27.5.1"
|
||||||
|
pretty-format "^27.5.1"
|
||||||
|
|
||||||
|
jest-get-type@^27.5.1:
|
||||||
|
version "27.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
|
||||||
|
integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
|
||||||
|
|
||||||
|
jest-matcher-utils@^27.0.0:
|
||||||
|
version "27.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab"
|
||||||
|
integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==
|
||||||
|
dependencies:
|
||||||
|
chalk "^4.0.0"
|
||||||
|
jest-diff "^27.5.1"
|
||||||
|
jest-get-type "^27.5.1"
|
||||||
|
pretty-format "^27.5.1"
|
||||||
|
|
||||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
@ -2544,6 +2587,15 @@ posthtml@^0.16.4, posthtml@^0.16.5:
|
|||||||
posthtml-parser "^0.10.0"
|
posthtml-parser "^0.10.0"
|
||||||
posthtml-render "^3.0.0"
|
posthtml-render "^3.0.0"
|
||||||
|
|
||||||
|
pretty-format@^27.0.0, pretty-format@^27.5.1:
|
||||||
|
version "27.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||||
|
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
ansi-styles "^5.0.0"
|
||||||
|
react-is "^17.0.1"
|
||||||
|
|
||||||
prop-types@^15.8.1:
|
prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
@ -2609,6 +2661,11 @@ react-is@^16.13.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
|
react-is@^17.0.1:
|
||||||
|
version "17.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
|
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||||
|
|
||||||
react-refresh@^0.9.0:
|
react-refresh@^0.9.0:
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
|
||||||
|
Loading…
Reference in New Issue
Block a user