n8n-editor-ui
Version:
Workflow Editor UI for n8n
462 lines (461 loc) • 15.8 kB
JavaScript
const wPost = (type, ...args) => postMessage({
type,
payload: args
});
const installAsyncProxy = function() {
const toss = function(...args) {
throw new Error(args.join(" "));
};
if (globalThis.window === globalThis) toss("This code cannot run from the main thread.", "Load it as a Worker from a separate Worker.");
else if (!navigator?.storage?.getDirectory) toss("This API requires navigator.storage.getDirectory.");
const state = Object.create(null);
state.verbose = 1;
const loggers = {
0: console.error.bind(console),
1: console.warn.bind(console),
2: console.log.bind(console)
};
const logImpl = (level, ...args) => {
if (state.verbose > level) loggers[level]("OPFS asyncer:", ...args);
};
const log = (...args) => logImpl(2, ...args);
const warn = (...args) => logImpl(1, ...args);
const error = (...args) => logImpl(0, ...args);
const __openFiles = Object.create(null);
const __implicitLocks = /* @__PURE__ */ new Set();
const getResolvedPath = function(filename, splitIt) {
const p = new URL(filename, "file://irrelevant").pathname;
return splitIt ? p.split("/").filter((v) => !!v) : p;
};
const getDirForFilename = async function f(absFilename, createDirs = false) {
const path = getResolvedPath(absFilename, true);
const filename = path.pop();
let dh = state.rootDir;
for (const dirName of path) if (dirName) dh = await dh.getDirectoryHandle(dirName, { create: !!createDirs });
return [dh, filename];
};
const closeSyncHandle = async (fh) => {
if (fh.syncHandle) {
log("Closing sync handle for", fh.filenameAbs);
const h = fh.syncHandle;
delete fh.syncHandle;
delete fh.xLock;
__implicitLocks.delete(fh.fid);
return h.close();
}
};
const closeSyncHandleNoThrow = async (fh) => {
try {
await closeSyncHandle(fh);
} catch (e) {
warn("closeSyncHandleNoThrow() ignoring:", e, fh);
}
};
const releaseImplicitLocks = async () => {
if (__implicitLocks.size) for (const fid of __implicitLocks) {
const fh = __openFiles[fid];
await closeSyncHandleNoThrow(fh);
log("Auto-unlocked", fid, fh.filenameAbs);
}
};
const releaseImplicitLock = async (fh) => {
if (fh.releaseImplicitLocks && __implicitLocks.has(fh.fid)) return closeSyncHandleNoThrow(fh);
};
class GetSyncHandleError extends Error {
constructor(errorObject, ...msg) {
super([
...msg,
": " + errorObject.name + ":",
errorObject.message
].join(" "), { cause: errorObject });
this.name = "GetSyncHandleError";
}
}
GetSyncHandleError.convertRc = (e, rc) => {
if (e instanceof GetSyncHandleError) {
if (e.cause.name === "NoModificationAllowedError" || e.cause.name === "DOMException" && 0 === e.cause.message.indexOf("Access Handles cannot")) return state.sq3Codes.SQLITE_BUSY;
else if ("NotFoundError" === e.cause.name) return state.sq3Codes.SQLITE_CANTOPEN;
} else if ("NotFoundError" === e?.name) return state.sq3Codes.SQLITE_CANTOPEN;
return rc;
};
const getSyncHandle = async (fh, opName) => {
if (!fh.syncHandle) {
const t = performance.now();
log("Acquiring sync handle for", fh.filenameAbs);
const maxTries = 6, msBase = state.asyncIdleWaitTime * 2;
let i = 1, ms = msBase;
for (;; ms = msBase * ++i) try {
fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
break;
} catch (e) {
if (i === maxTries) throw new GetSyncHandleError(e, "Error getting sync handle for", opName + "().", maxTries, "attempts failed.", fh.filenameAbs);
warn("Error getting sync handle for", opName + "(). Waiting", ms, "ms and trying again.", fh.filenameAbs, e);
Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms);
}
log("Got", opName + "() sync handle for", fh.filenameAbs, "in", performance.now() - t, "ms");
if (!fh.xLock) {
__implicitLocks.add(fh.fid);
log("Acquired implicit lock for", opName + "()", fh.fid, fh.filenameAbs);
}
}
return fh.syncHandle;
};
const storeAndNotify = (opName, value) => {
log(opName + "() => notify(", value, ")");
Atomics.store(state.sabOPView, state.opIds.rc, value);
Atomics.notify(state.sabOPView, state.opIds.rc);
};
const affirmNotRO = function(opName, fh) {
if (fh.readOnly) toss(opName + "(): File is read-only: " + fh.filenameAbs);
};
let flagAsyncShutdown = false;
const vfsAsyncImpls = {
"opfs-async-shutdown": async () => {
flagAsyncShutdown = true;
storeAndNotify("opfs-async-shutdown", 0);
},
mkdir: async (dirname) => {
let rc = 0;
try {
await getDirForFilename(dirname + "/filepart", true);
} catch (e) {
state.s11n.storeException(2, e);
rc = state.sq3Codes.SQLITE_IOERR;
}
storeAndNotify("mkdir", rc);
},
xAccess: async (filename) => {
let rc = 0;
try {
const [dh, fn] = await getDirForFilename(filename);
await dh.getFileHandle(fn);
} catch (e) {
state.s11n.storeException(2, e);
rc = state.sq3Codes.SQLITE_IOERR;
}
storeAndNotify("xAccess", rc);
},
xClose: async function(fid) {
const opName = "xClose";
__implicitLocks.delete(fid);
const fh = __openFiles[fid];
let rc = 0;
if (fh) {
delete __openFiles[fid];
await closeSyncHandle(fh);
if (fh.deleteOnClose) try {
await fh.dirHandle.removeEntry(fh.filenamePart);
} catch (e) {
warn("Ignoring dirHandle.removeEntry() failure of", fh, e);
}
} else {
state.s11n.serialize();
rc = state.sq3Codes.SQLITE_NOTFOUND;
}
storeAndNotify(opName, rc);
},
xDelete: async function(...args) {
storeAndNotify("xDelete", await vfsAsyncImpls.xDeleteNoWait(...args));
},
xDeleteNoWait: async function(filename, syncDir = 0, recursive = false) {
let rc = 0;
try {
while (filename) {
const [hDir, filenamePart] = await getDirForFilename(filename, false);
if (!filenamePart) break;
await hDir.removeEntry(filenamePart, { recursive });
if (4660 !== syncDir) break;
recursive = false;
filename = getResolvedPath(filename, true);
filename.pop();
filename = filename.join("/");
}
} catch (e) {
state.s11n.storeException(2, e);
rc = state.sq3Codes.SQLITE_IOERR_DELETE;
}
return rc;
},
xFileSize: async function(fid) {
const fh = __openFiles[fid];
let rc = 0;
try {
const sz = await (await getSyncHandle(fh, "xFileSize")).getSize();
state.s11n.serialize(Number(sz));
} catch (e) {
state.s11n.storeException(1, e);
rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR);
}
await releaseImplicitLock(fh);
storeAndNotify("xFileSize", rc);
},
xLock: async function(fid, lockType) {
const fh = __openFiles[fid];
let rc = 0;
const oldLockType = fh.xLock;
fh.xLock = lockType;
if (!fh.syncHandle) try {
await getSyncHandle(fh, "xLock");
__implicitLocks.delete(fid);
} catch (e) {
state.s11n.storeException(1, e);
rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_LOCK);
fh.xLock = oldLockType;
}
storeAndNotify("xLock", rc);
},
xOpen: async function(fid, filename, flags, opfsFlags) {
const opName = "xOpen";
const create = state.sq3Codes.SQLITE_OPEN_CREATE & flags;
try {
let hDir, filenamePart;
try {
[hDir, filenamePart] = await getDirForFilename(filename, !!create);
} catch (e) {
state.s11n.storeException(1, e);
storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
return;
}
if (state.opfsFlags.OPFS_UNLINK_BEFORE_OPEN & opfsFlags) try {
await hDir.removeEntry(filenamePart);
} catch (e) {}
const hFile = await hDir.getFileHandle(filenamePart, { create });
const fh = Object.assign(Object.create(null), {
fid,
filenameAbs: filename,
filenamePart,
dirHandle: hDir,
fileHandle: hFile,
sabView: state.sabFileBufView,
readOnly: !create && !!(state.sq3Codes.SQLITE_OPEN_READONLY & flags),
deleteOnClose: !!(state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags)
});
fh.releaseImplicitLocks = opfsFlags & state.opfsFlags.OPFS_UNLOCK_ASAP || state.opfsFlags.defaultUnlockAsap;
__openFiles[fid] = fh;
storeAndNotify(opName, 0);
} catch (e) {
error(opName, e);
state.s11n.storeException(1, e);
storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
}
},
xRead: async function(fid, n, offset64) {
let rc = 0, nRead;
const fh = __openFiles[fid];
try {
nRead = (await getSyncHandle(fh, "xRead")).read(fh.sabView.subarray(0, n), { at: Number(offset64) });
if (nRead < n) {
fh.sabView.fill(0, nRead, n);
rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
}
} catch (e) {
error("xRead() failed", e, fh);
state.s11n.storeException(1, e);
rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_READ);
}
await releaseImplicitLock(fh);
storeAndNotify("xRead", rc);
},
xSync: async function(fid, flags) {
const fh = __openFiles[fid];
let rc = 0;
if (!fh.readOnly && fh.syncHandle) try {
await fh.syncHandle.flush();
} catch (e) {
state.s11n.storeException(2, e);
rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
}
storeAndNotify("xSync", rc);
},
xTruncate: async function(fid, size) {
let rc = 0;
const fh = __openFiles[fid];
try {
affirmNotRO("xTruncate", fh);
await (await getSyncHandle(fh, "xTruncate")).truncate(size);
} catch (e) {
error("xTruncate():", e, fh);
state.s11n.storeException(2, e);
rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_TRUNCATE);
}
await releaseImplicitLock(fh);
storeAndNotify("xTruncate", rc);
},
xUnlock: async function(fid, lockType) {
let rc = 0;
const fh = __openFiles[fid];
if (fh.syncHandle && state.sq3Codes.SQLITE_LOCK_NONE === lockType) try {
await closeSyncHandle(fh);
} catch (e) {
state.s11n.storeException(1, e);
rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
}
storeAndNotify("xUnlock", rc);
},
xWrite: async function(fid, n, offset64) {
let rc;
const fh = __openFiles[fid];
try {
affirmNotRO("xWrite", fh);
rc = n === (await getSyncHandle(fh, "xWrite")).write(fh.sabView.subarray(0, n), { at: Number(offset64) }) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
} catch (e) {
error("xWrite():", e, fh);
state.s11n.storeException(1, e);
rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_WRITE);
}
await releaseImplicitLock(fh);
storeAndNotify("xWrite", rc);
}
};
const initS11n = () => {
if (state.s11n) return state.s11n;
const textDecoder = new TextDecoder(), textEncoder = new TextEncoder("utf-8"), viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
state.s11n = Object.create(null);
const TypeIds = Object.create(null);
TypeIds.number = {
id: 1,
size: 8,
getter: "getFloat64",
setter: "setFloat64"
};
TypeIds.bigint = {
id: 2,
size: 8,
getter: "getBigInt64",
setter: "setBigInt64"
};
TypeIds.boolean = {
id: 3,
size: 4,
getter: "getInt32",
setter: "setInt32"
};
TypeIds.string = { id: 4 };
const getTypeId = (v) => TypeIds[typeof v] || toss("Maintenance required: this value type cannot be serialized.", v);
const getTypeIdById = (tid) => {
switch (tid) {
case TypeIds.number.id: return TypeIds.number;
case TypeIds.bigint.id: return TypeIds.bigint;
case TypeIds.boolean.id: return TypeIds.boolean;
case TypeIds.string.id: return TypeIds.string;
default: toss("Invalid type ID:", tid);
}
};
state.s11n.deserialize = function(clear = false) {
const argc = viewU8[0];
const rc = argc ? [] : null;
if (argc) {
const typeIds = [];
let offset = 1, i, n, v;
for (i = 0; i < argc; ++i, ++offset) typeIds.push(getTypeIdById(viewU8[offset]));
for (i = 0; i < argc; ++i) {
const t = typeIds[i];
if (t.getter) {
v = viewDV[t.getter](offset, state.littleEndian);
offset += t.size;
} else {
n = viewDV.getInt32(offset, state.littleEndian);
offset += 4;
v = textDecoder.decode(viewU8.slice(offset, offset + n));
offset += n;
}
rc.push(v);
}
}
if (clear) viewU8[0] = 0;
return rc;
};
state.s11n.serialize = function(...args) {
if (args.length) {
const typeIds = [];
let i = 0, offset = 1;
viewU8[0] = args.length & 255;
for (; i < args.length; ++i, ++offset) {
typeIds.push(getTypeId(args[i]));
viewU8[offset] = typeIds[i].id;
}
for (i = 0; i < args.length; ++i) {
const t = typeIds[i];
if (t.setter) {
viewDV[t.setter](offset, args[i], state.littleEndian);
offset += t.size;
} else {
const s = textEncoder.encode(args[i]);
viewDV.setInt32(offset, s.byteLength, state.littleEndian);
offset += 4;
viewU8.set(s, offset);
offset += s.byteLength;
}
}
} else viewU8[0] = 0;
};
state.s11n.storeException = state.asyncS11nExceptions ? (priority, e) => {
if (priority <= state.asyncS11nExceptions) state.s11n.serialize([
e.name,
": ",
e.message
].join(""));
} : () => {};
return state.s11n;
};
const waitLoop = async function f() {
const opHandlers = Object.create(null);
for (let k of Object.keys(state.opIds)) {
const vi = vfsAsyncImpls[k];
if (!vi) continue;
const o = Object.create(null);
opHandlers[state.opIds[k]] = o;
o.key = k;
o.f = vi;
}
while (!flagAsyncShutdown) try {
if ("not-equal" !== Atomics.wait(state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime)) {
await releaseImplicitLocks();
continue;
}
const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #", opId);
const args = state.s11n.deserialize(true) || [];
if (hnd.f) await hnd.f(...args);
else error("Missing callback for opId", opId);
} catch (e) {
error("in waitLoop():", e);
}
};
navigator.storage.getDirectory().then(function(d) {
state.rootDir = d;
globalThis.onmessage = function({ data }) {
switch (data.type) {
case "opfs-async-init": {
const opt = data.args;
for (const k in opt) state[k] = opt[k];
state.verbose = opt.verbose ?? 1;
state.sabOPView = new Int32Array(state.sabOP);
state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
Object.keys(vfsAsyncImpls).forEach((k) => {
if (!Number.isFinite(state.opIds[k])) toss("Maintenance required: missing state.opIds[", k, "]");
});
initS11n();
log("init state", state);
wPost("opfs-async-inited");
waitLoop();
break;
}
case "opfs-async-restart":
if (flagAsyncShutdown) {
warn("Restarting after opfs-async-shutdown. Might or might not work.");
flagAsyncShutdown = false;
waitLoop();
}
break;
}
};
wPost("opfs-async-loaded");
}).catch((e) => error("error initializing OPFS asyncer:", e));
};
if (!globalThis.SharedArrayBuffer) wPost("opfs-unavailable", "Missing SharedArrayBuffer API.", "The server must emit the COOP/COEP response headers to enable that.");
else if (!globalThis.Atomics) wPost("opfs-unavailable", "Missing Atomics API.", "The server must emit the COOP/COEP response headers to enable that.");
else if (!globalThis.FileSystemHandle || !globalThis.FileSystemDirectoryHandle || !globalThis.FileSystemFileHandle || !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle || !navigator?.storage?.getDirectory) wPost("opfs-unavailable", "Missing required OPFS APIs.");
else installAsyncProxy();