Compare commits

...

29 Commits

Author SHA1 Message Date
semantic-release-bot 78746b45eb chore(release): 0.1.0-develop.1 [skip ci]
# [0.1.0-develop.1](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.0.1...v0.1.0-develop.1) (2023-07-18)

### Bug Fixes

* always launch worker inside of createModule ([7093494](7093494227))
* pass root key to module ([27830c1](27830c1397))
* update libweb ([e72075f](e72075f99a))
* update libweb ([4f507fe](4f507fe79e))
* update libweb ([b98ded6](b98ded63c8))
* update object data ([4408dae](4408dae8eb))

### Features

* initial version ([03f4083](03f4083674))
2023-07-18 21:44:25 +00:00
Derrick Hammer d912d7a848
ci: add repository to package.json 2023-07-18 17:42:09 -04:00
Derrick Hammer 3cc398c5f7
ci: add semantic-release script 2023-07-18 17:41:32 -04:00
Derrick Hammer 63fc62401b
refactor: split handleIncomingMessage to its own file, set activePortalMasterKey, call maybeInitDefaultPortals 2023-07-18 17:06:28 -04:00
Derrick Hammer fcca0e2cc2
dep: update deps 2023-07-18 17:04:38 -04:00
Derrick Hammer 4408dae8eb
fix: update object data 2023-07-18 10:44:39 -04:00
Derrick Hammer b914067e85
dep: update deps 2023-07-18 10:44:11 -04:00
Derrick Hammer 96c6eacf89
ci: add .presetterrc.json 2023-07-04 02:11:50 -04:00
Derrick Hammer 27830c1397
fix: pass root key to module 2023-07-04 02:09:30 -04:00
Derrick Hammer 7093494227
fix: always launch worker inside of createModule 2023-07-04 02:06:41 -04:00
Derrick Hammer 2671fa35a7
refactor: switch to using verifyCid to check if module is valid 2023-07-04 02:06:09 -04:00
Derrick Hammer dd27520a8e
ci: move to github actions 2023-07-04 02:04:50 -04:00
Derrick Hammer f2eb164ae5
chore: update LICENSE 2023-07-04 02:04:29 -04:00
Derrick Hammer 7d6a7bda47
refactor: update to new libraries/tooling 2023-07-02 12:04:47 -04:00
Derrick Hammer b287cc3aff
refactor: set active portals based on bootloaderPortals 2023-06-23 19:35:26 -04:00
Derrick Hammer e72075f99a
fix: update libweb 2023-06-23 19:34:47 -04:00
Derrick Hammer 4f507fe79e
fix: update libweb 2023-06-23 18:50:16 -04:00
Derrick Hammer 08e7ef098b
ci: update rollup plugin config 2023-06-23 18:49:58 -04:00
Derrick Hammer c8d7336a84
ci: add clean:rollup script 2023-06-23 18:34:45 -04:00
Derrick Hammer 3f1bd189d4
ci: add wasm rollup plugin 2023-06-23 07:56:45 -04:00
Derrick Hammer b98ded63c8
fix: update libweb 2023-06-23 07:44:08 -04:00
Derrick Hammer bc0bbac6dd
ci: update rollup config 2023-06-23 05:50:10 -04:00
Derrick Hammer 0f546cfda8
ci: syntax error 2023-06-23 05:18:04 -04:00
Derrick Hammer ea97e1a681
ci: set source dir 2023-06-23 05:16:44 -04:00
Derrick Hammer 65b911cdc4
ci: fix build script 2023-06-23 05:15:42 -04:00
Derrick Hammer 371bff2ef7
ci: add npm-shrinkwrap.json 2023-06-23 05:14:09 -04:00
Derrick Hammer 5102c3405d
chore: add credits 2023-06-23 05:13:20 -04:00
Derrick Hammer d1d7f90b02
ci: setup 2023-06-23 05:07:03 -04:00
Derrick Hammer 03f4083674
feat: initial version 2023-06-23 05:04:11 -04:00
15 changed files with 20503 additions and 1 deletions

13
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Build/Publish
on:
push:
branches:
- master
- develop
- develop-*
jobs:
main:
uses: lumeweb/github-node-deploy-workflow/.github/workflows/main.yml@master
secrets: inherit

8
.presetterrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"preset": [
"@lumeweb/presetter-kernel-module-preset"
],
"config": {
"browser": true
}
}

16
CHANGELOG.md Normal file
View File

@ -0,0 +1,16 @@
# [0.1.0-develop.1](https://git.lumeweb.com/LumeWeb/kernel/compare/v0.0.1...v0.1.0-develop.1) (2023-07-18)
### Bug Fixes
* always launch worker inside of createModule ([7093494](https://git.lumeweb.com/LumeWeb/kernel/commit/70934942275b6c53c0723b3e88421f86378310e5))
* pass root key to module ([27830c1](https://git.lumeweb.com/LumeWeb/kernel/commit/27830c139759e99006d0e99b8ddb80413344664c))
* update libweb ([e72075f](https://git.lumeweb.com/LumeWeb/kernel/commit/e72075f99aa4da9c040aa0300bc11688f81c714d))
* update libweb ([4f507fe](https://git.lumeweb.com/LumeWeb/kernel/commit/4f507fe79eeadbb237ac43a0021b983e086ddaed))
* update libweb ([b98ded6](https://git.lumeweb.com/LumeWeb/kernel/commit/b98ded63c8260d46873f27098f898da2aa7c00a1))
* update object data ([4408dae](https://git.lumeweb.com/LumeWeb/kernel/commit/4408dae8eb4f1c431cd8c8028c00d4bade680a89))
### Features
* initial version ([03f4083](https://git.lumeweb.com/LumeWeb/kernel/commit/03f4083674e143316084a765e1b6091d5bbcad52))

View File

@ -1,6 +1,7 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2023 Hammer Technologies LLC
Copyright (c) 2022 Skynet Labs
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -1,2 +1,3 @@
# kernel
Code is based on, and copies much of its work from https://github.com/SkynetLabs/skynet-kernel/tree/beta/kernel

19540
npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "@lumeweb/kernel",
"version": "0.1.0-develop.1",
"type": "module",
"repository": {
"type": "git",
"url": "gitea@git.lumeweb.com:LumeWeb/kernel.git"
},
"devDependencies": {
"@lumeweb/presetter-kernel-module-preset": "^0.1.0-develop.43",
"binconv": "^0.2.0",
"presetter": "*",
"semantic-release": "^21.0.5"
},
"readme": "ERROR: No README data found!",
"scripts": {
"prepare": "presetter bootstrap",
"build": "run build",
"semantic-release": "semantic-release"
},
"dependencies": {
"@lumeweb/libkernel": "0.1.0-develop.19",
"@lumeweb/libweb": "0.2.0-develop.26",
"binconv": "^0.2.0"
}
}

31
src/err.ts Normal file
View File

@ -0,0 +1,31 @@
// notableErrors is a persistent list of errors that should be checked after
// testing. You should only add to this array in the event of an error that
// indicates a bug with the kernel.
const notableErrors: string[] = [];
// respondErr will send an error response to the caller that closes out the
// query for the provided nonce. The extra inputs of 'messagePortal' and
// 'isWorker' are necessary to handle the fact that the MessageEvent you get
// from a worker message is different from the MessageEvent you get from a
// window message, and also from the fact that postMessage has different
// arguments depending on whether the messagePortal is a worker or a window.
function respondErr(
event: MessageEvent,
messagePortal: any,
isWorker: boolean,
err: string,
) {
const message = {
nonce: event.data.nonce,
method: "response",
data: {},
err,
};
if (isWorker === true) {
messagePortal.postMessage(message);
} else {
messagePortal.postMessage(message, event.origin);
}
}
export { notableErrors, respondErr };

39
src/index.ts Normal file
View File

@ -0,0 +1,39 @@
import { logLargeObjects } from "./logLargeState.js";
import { log, logErr } from "./log.js";
import { KERNEL_DISTRO, KERNEL_VERSION } from "./version.js";
import {
maybeInitDefaultPortals,
setActivePortalMasterKey,
} from "@lumeweb/libweb";
import { Client } from "@lumeweb/libportal";
import { addContextToErr } from "@lumeweb/libkernel";
import { handleIncomingMessage } from "./message.js";
import { activeKey } from "./key.js";
declare global {
interface Window {
bootloaderPortals: Client[];
}
}
// Kick off the thread that will periodically log all of the large objects in
// the kernel, so that it's easier to check for memory leaks.
logLargeObjects();
// Write a log that declares the kernel version and distribution.
log("init", "Lume Web Kernel v" + KERNEL_VERSION + "-" + KERNEL_DISTRO);
/*
Try to load either our saved portal(s) or the default portal(s)
*/
setActivePortalMasterKey(activeKey);
let [, portalLoadErr] = maybeInitDefaultPortals();
if (portalLoadErr) {
let err = addContextToErr(portalLoadErr, "unable to init portals");
logErr(err);
}
if (!portalLoadErr) {
window.addEventListener("message", handleIncomingMessage);
}

5
src/key.ts Normal file
View File

@ -0,0 +1,5 @@
import { hexToBytes } from "@lumeweb/libweb";
export const activeKey = hexToBytes(
window.localStorage.getItem("key") as string,
);

31
src/log.ts Normal file
View File

@ -0,0 +1,31 @@
import { objAsString } from "@lumeweb/libweb";
// wLog is a wrapper for the log and logErr functions, to deduplicate code.
//
// TODO: Need to implement a tag system for the logging. We will use the
// dashboard to control logging messages and verbosity.
function wLog(isErr: boolean, tag: string, ...inputs: any) {
let message = "[lumeweb-kernel]\n" + tag;
for (let i = 0; i < inputs.length; i++) {
message += "\n";
message += objAsString(inputs[i]);
}
window.parent.postMessage(
{
method: "log",
data: {
isErr,
message,
},
},
"*",
);
}
function log(tag: string, ...inputs: any) {
wLog(false, tag, ...inputs);
}
function logErr(tag: string, ...inputs: any) {
wLog(true, tag, ...inputs);
}
export { log, logErr };

28
src/logLargeState.ts Normal file
View File

@ -0,0 +1,28 @@
import { notableErrors } from "./err.js";
import { log } from "./log.js";
import { modules, modulesLoading, queries } from "./queries.js";
// Set up a loop that will periodically log all of the large objects in the
// kernel, for the sake of making detection and debugging easier in the event
// of a
let waitTime = 30000;
function logLargeObjects() {
const queriesLenStr = Object.keys(queries).length.toString();
const modulesLenStr = Object.keys(modules).length.toString();
const modulesLoadingLenStr = Object.keys(modulesLoading).length.toString();
log(
"open queries :: open modules :: modules loading :: notable errors : " +
queriesLenStr +
" :: " +
modulesLenStr +
" :: " +
modulesLoadingLenStr +
" :: " +
notableErrors.length,
);
waitTime *= 1.25;
setTimeout(logLargeObjects, waitTime);
}
setTimeout(logLargeObjects, waitTime);
export { logLargeObjects };

111
src/message.ts Normal file
View File

@ -0,0 +1,111 @@
// Overwrite the handleIncomingMessage function that gets called at the end of the
// event handler, allowing us to support custom messages.
import { logErr } from "./log.js";
import { KERNEL_DISTRO, KERNEL_VERSION } from "./version.js";
import { notableErrors, respondErr } from "./err.js";
import { handleModuleCall, handleQueryUpdate } from "./queries.js";
export const handleIncomingMessage = function (event: any) {
// Ignore all messages from ourself.
if (event.source === window) {
return;
}
// Input validation.
if (!("method" in event.data)) {
logErr("handleIncomingMessage", "kernel request is missing 'method' field");
return;
}
if (!("nonce" in event.data)) {
logErr(
"handleIncomingMessage",
"message sent to kernel with no nonce field",
event.data,
);
return;
}
// Establish a debugging handler that a developer can call to verify
// that round-trip communication has been correctly programmed between
// the kernel and the calling application.
//
// It was easier to inline the message than to abstract it.
if (event.data.method === "version") {
event.source.postMessage(
{
nonce: event.data.nonce,
method: "response",
err: null,
data: {
distribution: KERNEL_DISTRO,
version: KERNEL_VERSION,
},
},
event.origin,
);
return;
}
// Establish a debugging handler to return any noteworthy errors that the
// kernel has encountered. This is mainly intended to be used by the test
// suite.
if (event.data.method === "checkErrs") {
event.source.postMessage(
{
nonce: event.data.nonce,
method: "response",
err: null,
data: {
errs: notableErrors,
},
},
event.origin,
);
return;
}
// Establish handlers for the major kernel methods.
if (event.data.method === "moduleCall") {
// Check for a domain. If the message was sent by a browser
// extension, we trust the domain provided by the extension,
// otherwise we use the domain of the parent as the domain.
// This does mean that the kernel is trusting that the user has
// no malicious browser extensions, as we aren't checking for
// **which** extension is sending the message, we are only
// checking that the message is coming from a browser
// extension.
if (event.origin.startsWith("moz") && !("domain" in event.data)) {
logErr(
"moduleCall",
"caller is an extension, but no domain was provided",
);
respondErr(
event,
event.source,
false,
"caller is an extension, but not domain was provided",
);
return;
}
let domain;
if (event.origin.startsWith("moz")) {
domain = event.data.domain;
} else {
domain = new URL(event.origin).hostname;
}
handleModuleCall(event, event.source, domain, false);
return;
}
if (event.data.method === "queryUpdate") {
handleQueryUpdate(event);
return;
}
// Unrecognized method, reject the query.
respondErr(
event,
event.source,
false,
"unrecognized method: " + event.data.method,
);
};

641
src/queries.ts Normal file
View File

@ -0,0 +1,641 @@
import { notableErrors, respondErr } from "./err.js";
import { log, logErr } from "./log.js";
import { activeKey } from "./key.js";
import { KERNEL_DISTRO, KERNEL_VERSION } from "./version.js";
import {
addContextToErr,
bufToB64,
encodeU64,
Err,
objAsString,
sha512,
} from "@lumeweb/libkernel";
import { deriveChildKey, downloadObject, verifyCid } from "@lumeweb/libweb";
import type { moduleQuery, presentKeyData } from "@lumeweb/libkernel/module";
import { readableStreamToUint8Array } from "binconv";
// WorkerLaunchFn is the type signature of the function that launches the
// worker to set up for processing a query.
type WorkerLaunchFn = () => [Worker, Err];
// modules is a hashmap that maps from a domain to the module that handles
// queries to that domain. It maintains the domain and URL of the module so
// that the worker doesn't need to be downloaded multiple times to keep
// launching queries.
//
// a new worker gets launched for every query.
interface Module {
domain: string;
url: string;
launchWorker: WorkerLaunchFn;
worker?: Worker;
}
// OpenQuery holds all of the information necessary for managing an open query.
interface OpenQuery {
isWorker: boolean;
domain: string;
source: any;
dest: Worker;
nonce: string;
origin: string;
}
// Define the stateful variables for managing the modules. We track the set of
// queries that are in progress, the set of skapps that are known to the
// kernel, the set of modules that we've downloaded, and the set of modules
// that are actively being downloaded.
let queriesNonce = 0;
const queries = {} as any;
const skapps = {} as any;
const modules = {} as any;
const modulesLoading = {} as any;
// Create a standard message handler for messages coming from workers.
//
// TODO: If the worker makes a mistake or has a bug that makes it seem
// unstable, we should create some sort of debug log that can be viewed from
// the kernel debug/control panel. We'll need to make sure the debug logs don't
// consume too much memory, and we'll need to terminate workers that are
// bugging out.
//
// TODO: Set up a ratelimiting system for modules making logs, we don't want
// modules to be able to pollute the kernel and cause instability by logging
// too much.
//
// TODO: Need to check that the postMessage call in respondErr isn't going to
// throw or cause issuse in the event that the worker who sent the message has
// been terminated.
//
// TODO: We probably need to have timeouts for queries, if a query doesn't send
// an update after a certain amount of time we drop it.
function handleWorkerMessage(event: MessageEvent, mod: Module, worker: Worker) {
// TODO: Use of respondErr here may not be correct, should only be using
// respondErr for functions that are expecting a response and aren't
// already part of a separate query. If they are part of a separate query
// we need to close that query out gracefully.
// Perform input verification for a worker message.
if (!("method" in event.data)) {
logErr("worker", mod.domain, "received worker message with no method");
respondErr(event, worker, true, "received message with no method");
return;
}
// Check whether this is a logging call.
if (event.data.method === "log") {
// Perform the input verification for logging.
if (!("data" in event.data)) {
logErr(
"worker",
mod.domain,
"received worker log message with no data field",
);
respondErr(
event,
worker,
true,
"received log messsage with no data field",
);
return;
}
if (typeof event.data.data.message !== "string") {
logErr(
"worker",
mod.domain,
"worker log data.message is not of type 'string'",
);
respondErr(
event,
worker,
true,
"received log messsage with no message field",
);
return;
}
if (event.data.data.isErr === undefined) {
event.data.data.isErr = false;
}
if (typeof event.data.data.isErr !== "boolean") {
logErr(
"worker",
mod.domain,
"worker log data.isErr is not of type 'boolean'",
);
respondErr(
event,
worker,
true,
"received log messsage with invalid isErr field",
);
return;
}
// Send the log to the parent so that the log can be put in the
// console.
if (event.data.data.isErr === false) {
log("worker", "[" + mod.domain + "]", event.data.data.message);
} else {
logErr("worker", "[" + mod.domain + "]", event.data.data.message);
}
return;
}
// Check for a nonce - log is the only message from a worker that does not
// need a nonce.
if (!("nonce" in event.data)) {
event.data.nonce = "N/A";
logErr(
"worker",
mod.domain,
"worker sent a message with no nonce",
event.data,
);
respondErr(event, worker, true, "received message with no nonce");
return;
}
// Handle a version request.
if (event.data.method === "version") {
worker.postMessage({
nonce: event.data.nonce,
method: "response",
err: null,
data: {
distribution: KERNEL_DISTRO,
version: KERNEL_VERSION,
err: null,
},
});
return;
}
// Handle a call from the worker to another module.
if (event.data.method === "moduleCall") {
handleModuleCall(event, worker, mod.domain, true);
return;
}
// The only other methods allowed are the queryUpdate, responseUpdate,
// and response methods.
const isQueryUpdate = event.data.method === "queryUpdate";
const isResponseUpdate = event.data.method === "responseUpdate";
const isResponse = event.data.method === "response";
if (isQueryUpdate || isResponseUpdate || isResponse) {
handleModuleResponse(event, mod, worker);
return;
}
// We don't know what this message was.
logErr(
"worker",
mod.domain,
"received message from worker with unrecognized method",
);
}
// createModule will create a module from the provided worker code and domain.
// This call does not launch the worker, that should be done separately.
async function createModule(
workerCode: Uint8Array | ReadableStream,
domain: string,
): Promise<[Module | null, Err]> {
if (workerCode instanceof ReadableStream) {
try {
workerCode = await readableStreamToUint8Array(workerCode);
} catch (e) {
return [null, e];
}
}
// Generate the URL for the worker code.
const url = URL.createObjectURL(new Blob([workerCode]));
// Create the module object.
const mod: Module = {
domain,
url,
launchWorker: function (): [Worker, Err] {
return launchWorker(mod);
},
};
// Start worker
const [worker, err] = mod.launchWorker();
if (err !== null) {
return [{} as Module, addContextToErr(err, "unable to launch worker")];
}
mod.worker = worker;
return [mod, null];
}
// launchWorker will launch a worker and perform all the setup so that the
// worker is ready to receive a query.
function launchWorker(mod: Module): [Worker, Err] {
// Create and launch the worker.
let worker: Worker;
try {
worker = new Worker(mod.url);
} catch (err: any) {
logErr("worker", mod.domain, "unable to create worker", mod.domain, err);
return [
{} as Worker,
addContextToErr(objAsString(err), "unable to create worker"),
];
}
// Set the onmessage and onerror functions.
worker.onmessage = function (event: MessageEvent) {
handleWorkerMessage(event, mod, worker);
};
worker.onerror = function (event: ErrorEvent) {
const errStr =
objAsString(event.message) +
"\n" +
objAsString(event.error) +
"\n" +
objAsString(event);
logErr(
"worker",
mod.domain,
addContextToErr(errStr, "received onerror event"),
);
};
// Send the key to the module.
const path = "moduleKeyDerivation" + mod.domain;
const moduleKey = deriveChildKey(activeKey, path);
const msgData: presentKeyData = {
key: moduleKey,
rootPrivateKey: activeKey,
};
const msg: moduleQuery = {
method: "presentKey",
domain: "root",
data: msgData,
};
worker.postMessage(msg);
return [worker, null];
}
// handleModuleCall will handle a callModule message sent to the kernel from an
// extension or webpage.
function handleModuleCall(
event: MessageEvent,
messagePortal: any,
callerDomain: string,
isWorker: boolean,
) {
if (!("data" in event.data) || !("module" in event.data.data)) {
logErr(
"moduleCall",
"received moduleCall with no module field in the data",
event.data,
);
respondErr(
event,
messagePortal,
isWorker,
"moduleCall is missing 'module' field: " + JSON.stringify(event.data),
);
return;
}
if (
typeof event.data.data.module !== "string" ||
!verifyCid(event.data.data.module)
) {
logErr("moduleCall", "received moduleCall with malformed module");
respondErr(
event,
messagePortal,
isWorker,
"'module' field in moduleCall is expected to be a base58 encoded blake3 hash + filesize",
);
return;
}
if (!("method" in event.data.data)) {
logErr(
"moduleCall",
"received moduleCall without a method set for the module",
);
respondErr(
event,
messagePortal,
isWorker,
"no 'data.method' specified, module does not know what method to run",
);
return;
}
if (typeof event.data.data.method !== "string") {
logErr(
"moduleCall",
"recieved moduleCall with malformed method",
event.data,
);
respondErr(
event,
messagePortal,
isWorker,
"'data.method' needs to be a string",
);
return;
}
if (event.data.data.method === "presentSeed") {
logErr(
"moduleCall",
"received malicious moduleCall - only root is allowed to use presentSeed method",
);
respondErr(
event,
messagePortal,
isWorker,
"presentSeed is a priviledged method, only root is allowed to use it",
);
return;
}
if (!("data" in event.data.data)) {
logErr("moduleCall", "received moduleCall with no input for the module");
respondErr(
event,
messagePortal,
isWorker,
"no field data.data in moduleCall, data.data contains the module input",
);
return;
}
// TODO: Load any overrides.
const finalModule = event.data.data.module; // Can change with overrides.
const moduleDomain = event.data.data.module; // Does not change with overrides.
// Define a helper function to create a new query to the module. It will
// both open a query on the module and also send an update message to the
// caller with the kernel nonce for this query so that the caller can
// perform query updates.
const newModuleQuery = function (mod: Module) {
let worker = mod.worker!;
// Get the nonce for this query. The nonce is a
// cryptographically secure string derived from a number and
// the user's seed. We use 'kernelNonceSalt' as a salt to
// namespace the nonces and make sure other processes don't
// accidentally end up using the same hashes.
const nonceSalt = new TextEncoder().encode("kernelNonceSalt");
const [nonceBytes] = encodeU64(BigInt(queriesNonce));
const noncePreimage = new Uint8Array(
nonceSalt.length + activeKey.length + nonceBytes.length,
);
noncePreimage.set(nonceSalt, 0);
noncePreimage.set(activeKey, nonceSalt.length);
noncePreimage.set(nonceBytes, nonceSalt.length + activeKey.length);
const nonce = bufToB64(sha512(noncePreimage));
queriesNonce = queriesNonce + 1;
queries[nonce] = {
isWorker,
domain: callerDomain,
source: messagePortal,
dest: worker,
nonce: event.data.nonce,
origin: event.origin,
} as OpenQuery;
// Send the message to the worker to start the query.
worker.postMessage({
nonce: nonce,
domain: callerDomain,
method: event.data.data.method,
data: event.data.data.data,
});
// If the caller is asking for the kernel nonce for this query,
// send the kernel nonce. We don't always send the kernel nonce
// because messages have material overhead.
if (event.data.sendKernelNonce === true) {
const msg = {
nonce: event.data.nonce,
method: "responseNonce",
data: {
nonce,
},
};
if (isWorker) {
messagePortal.postMessage(msg);
} else {
messagePortal.postMessage(msg, event.origin);
}
}
};
// Check the worker pool to see if this module is already available.
if (moduleDomain in modules) {
const module = modules[moduleDomain];
newModuleQuery(module);
return;
}
// Check if another thread is already fetching the module.
if (moduleDomain in modulesLoading) {
const p = modulesLoading[moduleDomain];
p.then((errML: Err) => {
if (errML !== null) {
respondErr(
event,
messagePortal,
isWorker,
addContextToErr(errML, "module could not be loaded"),
);
return;
}
const module = modules[moduleDomain];
newModuleQuery(module);
});
return;
}
// Fetch the module in a background thread, and launch the query once the
// module is available.
modulesLoading[moduleDomain] = new Promise(async (resolve) => {
// TODO: Check localStorage for the module.
// Download the code for the worker.
const [moduleData, errDS] = await downloadObject(finalModule);
if (errDS !== null) {
const err = addContextToErr(errDS, "unable to load module");
respondErr(event, messagePortal, isWorker, err);
resolve(err);
delete modulesLoading[moduleDomain];
return;
}
// The call to download the skylink is async. That means it's possible that
// some other thread created the module successfully and already added it.
// Based on the rest of the code, this should not be possible, but we check
// for it anyway at runtime so that any concurrency bugs will be made
// visible through the `notableErrors` field.
//
// This check is mainly here as a verification that the rest of the kernel
// code is correct.
if (moduleDomain in modules) {
// Though this is an error, we do already have the module so we
// use the one we already loaded.
logErr("a module that was already loaded has been loaded");
notableErrors.push("module loading experienced a race condition");
const mod = modules[moduleDomain];
newModuleQuery(mod);
resolve(null);
return;
}
// TODO: Save the result to localStorage. Can't do that until
// subscriptions are in place so that localStorage can sync
// with any updates from the remote module.
// Create a new module.
const [mod, errCM] = await createModule(moduleData, moduleDomain);
if (errCM !== null) {
const err = addContextToErr(errCM, "unable to create module");
respondErr(event, messagePortal, isWorker, err);
resolve(err);
delete modulesLoading[moduleDomain];
return;
}
modules[moduleDomain] = mod as Module;
newModuleQuery(mod as Module);
resolve(null);
delete modulesLoading[moduleDomain];
});
}
function handleModuleResponse(
event: MessageEvent,
mod: Module,
worker: Worker,
) {
// TODO: Need to figure out what to do with the errors here. Do we call
// 'respondErr'? That doesn't seem correct. It's not correct because if we
// end a query we need to let both sides know that the query was killed by
// the kernel.
// Technically the caller already computed these values, but it's easier to
// compute them again than to pass them as function args.
const isQueryUpdate = event.data.method === "queryUpdate";
const isResponse = event.data.method === "response";
// Check that the data field is present.
if (!("data" in event.data)) {
logErr(
"worker",
mod.domain,
"received response or update from worker with no data field",
);
return;
}
// Grab the query information so that we can properly relay the worker
// response to the original caller.
if (!(event.data.nonce in queries)) {
// If there's no corresponding query and this is a response, send an
// error.
if (isResponse === true) {
logErr("worker", mod.domain, "received response for an unknown nonce");
return;
}
// If there's no responding query and this isn't a response, it could
// just be an accident. queryUpdates and responseUpdates are async and
// can therefore be sent before both sides know that a query has been
// closed but not get processed untila afterwards.
//
// This can't happen with a 'response' message because the response
// message is the only message that can close the query, and there's
// only supposed to be one response message.
return;
}
// If the message is a query update, relay the update to the worker.
if (isQueryUpdate) {
const dest = queries[event.data.nonce].dest;
dest.postMessage({
nonce: event.data.nonce,
method: event.data.method,
data: event.data.data,
});
return;
}
// Check that the err field is being used correctly for response messages.
if (isResponse) {
// Check that the err field exists.
if (!("err" in event.data)) {
logErr(
"worker",
mod.domain,
"got response from worker with no err field",
);
return;
}
// Check that exactly one of 'err' and 'data' are null.
const errNull = event.data.err === null;
const dataNull = event.data.data === null;
if (errNull === dataNull) {
logErr("worker", mod.domain, "exactly one of err and data must be null");
return;
}
}
// We are sending either a response message or a responseUpdate message,
// all other possibilities have been handled.
const sourceIsWorker = queries[event.data.nonce].isWorker;
const sourceNonce = queries[event.data.nonce].nonce;
const source = queries[event.data.nonce].source;
const origin = queries[event.data.nonce].origin;
const msg: any = {
nonce: sourceNonce,
method: event.data.method,
data: event.data.data,
};
// For responses only, set an error and close out the query by deleting it
// from the query map.
if (isResponse) {
msg["err"] = event.data.err;
delete queries[event.data.nonce];
}
if (sourceIsWorker === true) {
source.postMessage(msg);
} else {
source.postMessage(msg, origin);
}
}
function handleQueryUpdate(event: MessageEvent) {
// Check that the module still exists before sending a queryUpdate to
// the module.
if (!(event.data.nonce in queries)) {
logErr(
"auth",
"received queryUpdate but nonce is not recognized",
event.data,
queries,
);
return;
}
const dest = queries[event.data.nonce].dest;
dest.postMessage({
nonce: event.data.nonce,
method: event.data.method,
data: event.data.data,
});
}
export {
Module,
handleModuleCall,
handleModuleResponse,
handleQueryUpdate,
modules,
modulesLoading,
queries,
};

11
src/version.ts Normal file
View File

@ -0,0 +1,11 @@
// Set the distribution and version of this kernel. There may be other versions
// of the kernel in the world produced by other development teams, so openly
// declaring the version number and development team allows other pieces of
// software to determine what features are or are not supported.
//
// At some point we may want something like a capabilities array, but the
// ecosystem isn't mature enough to need that.
const KERNEL_DISTRO = "Lume Web";
const KERNEL_VERSION = "0.1.0";
export { KERNEL_DISTRO, KERNEL_VERSION };