Compare commits
No commits in common. "v0.1.0-develop.1" and "v0.0.1" have entirely different histories.
v0.1.0-dev
...
v0.0.1
|
@ -1,45 +0,0 @@
|
|||
version: 2.1
|
||||
|
||||
orbs:
|
||||
node: circleci/node@5.1.0
|
||||
ssh: credijusto/ssh@0.5.2
|
||||
workflows:
|
||||
release:
|
||||
jobs:
|
||||
- node/run:
|
||||
name: build
|
||||
npm-run: build
|
||||
post-steps:
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- lib/
|
||||
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- /^develop-.*$/
|
||||
- node/run:
|
||||
name: release
|
||||
npm-run: semantic-release
|
||||
requires:
|
||||
- build
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- /^develop-.*$/
|
||||
|
||||
context:
|
||||
- publish
|
||||
setup:
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "47:cf:a1:17:d9:81:8e:c5:51:e5:53:c8:33:e4:33:b9"
|
||||
- ssh/ssh-add-host:
|
||||
host_url: GITEA_HOST
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"preset": [
|
||||
"presetter-preset-strict"
|
||||
],
|
||||
"config": {
|
||||
"tsconfig": {
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ES2021",
|
||||
"dom"
|
||||
]
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": false
|
||||
}
|
||||
},
|
||||
"variable": {
|
||||
"source": "src"
|
||||
}
|
||||
}
|
22
.releaserc
22
.releaserc
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/changelog"
|
||||
],
|
||||
"@semantic-release/npm",
|
||||
"@semantic-release/git",
|
||||
],
|
||||
"branches": [
|
||||
"master",
|
||||
{
|
||||
name: "develop",
|
||||
prerelease: true
|
||||
},
|
||||
{
|
||||
name: "develop-*",
|
||||
prerelease: true
|
||||
},
|
||||
]
|
||||
}
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,11 +0,0 @@
|
|||
# [0.1.0-develop.1](https://git.lumeweb.com/LumeWeb/libkmodule/compare/v0.0.1...v0.1.0-develop.1) (2023-06-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* seed is now key ([e4dd9e7](https://git.lumeweb.com/LumeWeb/libkmodule/commit/e4dd9e715cab312dc6a19736f459533f3196cade))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Initial version ([b68f2c0](https://git.lumeweb.com/LumeWeb/libkmodule/commit/b68f2c0281e476f4e9675299723aa36ea0e78027))
|
3
LICENSE
3
LICENSE
|
@ -1,7 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Hammer Technologies LLC
|
||||
Copyright (c) 2022 Skynet Labs
|
||||
Copyright (c) <year> <copyright holders>
|
||||
|
||||
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:
|
||||
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
# libkmodule
|
||||
|
||||
Library is based on, and copies much of its work from https://github.com/SkynetLabs/skynet-kernel/tree/beta/libs/libkmodule
|
||||
|
|
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"name": "@lumweb/libkmodule",
|
||||
"version": "0.1.0-develop.1",
|
||||
"main": "lib/index.js",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "gitea@git.lumeweb.com:LumeWeb/libkmodule.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"presetter": "^4.0.1",
|
||||
"presetter-preset-strict": "^4.0.0",
|
||||
"semantic-release": "^21.0.5"
|
||||
},
|
||||
"readme": "ERROR: No README data found!",
|
||||
"scripts": {
|
||||
"prepare": "presetter bootstrap",
|
||||
"build": "run build",
|
||||
"semantic-release": "semantic-release"
|
||||
},
|
||||
"files": [
|
||||
"lib/**"
|
||||
],
|
||||
"dependencies": {
|
||||
"@lumeweb/libweb": "0.2.0-develop.3"
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export { log, logErr } from "./log.js";
|
||||
export { ActiveQuery, addHandler, handleMessage } from "./messages.js";
|
||||
export { callModule, connectModule, newKernelQuery } from "./queries.js";
|
||||
export { getDataFromKernel, getKey } from "./key.js";
|
||||
export { moduleQuery, presentKeyData } from "./types.js";
|
||||
export { DataFn, Err, addContextToErr, objAsString } from "@lumeweb/libweb";
|
38
src/key.ts
38
src/key.ts
|
@ -1,38 +0,0 @@
|
|||
import { ActiveQuery, DataFn } from "./messages.js";
|
||||
|
||||
// Define a set of helper variables that track whether the key has been
|
||||
// received by the kernel yet.
|
||||
let resolveKey: DataFn;
|
||||
const keyPromise: Promise<Uint8Array> = new Promise((resolve) => {
|
||||
resolveKey = resolve;
|
||||
});
|
||||
|
||||
// dataFromKernel will hold any data that is sent by the kernel in the
|
||||
// 'presentKey' call that happens at startup.
|
||||
//
|
||||
// dataFromKernel should not be accessed until 'keyPromise' has been resolved.
|
||||
let dataFromKernel: any;
|
||||
|
||||
// getKey will return a promise that resolves when the key is available.
|
||||
function getKey(): Promise<Uint8Array> {
|
||||
return keyPromise;
|
||||
}
|
||||
|
||||
// getDataFromKernel will resolve with the data that was provided by the kernel
|
||||
// in 'presentKey' once that data is available.
|
||||
function getDataFromKernel(): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
keyPromise.then(() => {
|
||||
resolve(dataFromKernel);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// handlePresentKey will accept a key from the kernel and unblock any method
|
||||
// that is waiting for the key.
|
||||
function handlePresentKey(aq: ActiveQuery) {
|
||||
dataFromKernel = aq.callerInput;
|
||||
resolveKey(aq.callerInput.key);
|
||||
}
|
||||
|
||||
export { getDataFromKernel, getKey, handlePresentKey };
|
42
src/log.ts
42
src/log.ts
|
@ -1,42 +0,0 @@
|
|||
import { objAsString } from "@lumeweb/libweb";
|
||||
|
||||
// logHelper is a helper function that runs the code for both log and logErr.
|
||||
// It takes a boolean indiciating whether the log should be an error, and then
|
||||
// it stringifies all of the reamining inputs and sends them to the kernel in a
|
||||
// log message.
|
||||
function logHelper(isErr: boolean, ...inputs: any) {
|
||||
let message = "";
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
if (i !== 0) {
|
||||
message += "\n";
|
||||
}
|
||||
message += objAsString(inputs[i]);
|
||||
}
|
||||
postMessage({
|
||||
method: "log",
|
||||
data: {
|
||||
isErr,
|
||||
message,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// log is a helper function to send a bunch of inputs to the kernel serialized
|
||||
// as a log message. Note that any inputs which cannot be stringified using
|
||||
// JSON.stringify will be substituted with a placeholder string indicating that
|
||||
// the input could not be stringified.
|
||||
function log(...inputs: any) {
|
||||
console.log(...inputs);
|
||||
logHelper(false, ...inputs);
|
||||
}
|
||||
|
||||
// logErr is a helper function to send a bunch of inputs to the kernel
|
||||
// serialized as an error log message. Note that any inputs which cannot be
|
||||
// stringified using JSON.stringify will be substituted with a placeholder
|
||||
// string indicating that the input could not be stringified.
|
||||
function logErr(...inputs: any) {
|
||||
console.error(...inputs);
|
||||
logHelper(true, ...inputs);
|
||||
}
|
||||
|
||||
export { log, logErr };
|
|
@ -1,10 +0,0 @@
|
|||
import { ActiveQuery } from "./messages.js";
|
||||
|
||||
// handleNoOp create a no-op function for the module that allows the module to
|
||||
// be "warmed up", meaning the kernel will stick the module into the cache so
|
||||
// that it loads faster when a user actually needs the module.
|
||||
function handleNoOp(aq: ActiveQuery) {
|
||||
aq.respond({ success: true });
|
||||
}
|
||||
|
||||
export { handleNoOp };
|
255
src/messages.ts
255
src/messages.ts
|
@ -1,255 +0,0 @@
|
|||
import { logErr } from "./log.js";
|
||||
import { handleNoOp } from "./messageNoOp.js";
|
||||
import {
|
||||
clearIncomingQuery,
|
||||
getSetReceiveUpdate,
|
||||
handleQueryUpdate,
|
||||
handleResponse,
|
||||
handleResponseNonce,
|
||||
handleResponseUpdate,
|
||||
} from "./queries.js";
|
||||
import { handlePresentKey } from "./key.js";
|
||||
import { DataFn, ErrFn, addContextToErr, objAsString } from "@lumeweb/libweb";
|
||||
|
||||
// handlerFn takes an ActiveQuery as input and has no return value. The return
|
||||
// is expected to come in the form of calling aq.accept or aq.reject.
|
||||
type handlerFn = (aq: ActiveQuery) => void;
|
||||
|
||||
// ActiveQuery is an object that gets provided to the handler of a query and
|
||||
// contains all necessary elements for interacting with the query.
|
||||
interface ActiveQuery {
|
||||
// callerInput is arbitrary input provided by the caller that is not
|
||||
// checked by the kernel. Modules should verify the callerInput before
|
||||
// using any fields.
|
||||
callerInput: any;
|
||||
|
||||
// accept and reject are functions that will send response messages
|
||||
// that close out the query. accept can take an arbitrary object as
|
||||
// input, reject should always be a string.
|
||||
respond: DataFn;
|
||||
reject: ErrFn;
|
||||
|
||||
// domain is a field provided by the kernel that informs the module who
|
||||
// the caller is. The module can use the domain to make access control
|
||||
// decisions, and determine if a particular caller should be allowed to
|
||||
// use a particular API.
|
||||
domain: string;
|
||||
|
||||
// sendUpdate is used for sending responseUpdate messages to the
|
||||
// caller. These messages can contain arbitrary information.
|
||||
sendUpdate: DataFn;
|
||||
|
||||
// setReceiveUpdate is part of a handshake that needs to be performed
|
||||
// to receive queryUpdates from the caller. It is a function that takes
|
||||
// another function as input. The function provided as input is the
|
||||
// function that will be called to process incoming queryUpdates.
|
||||
setReceiveUpdate?: (receiveUpdate: DataFn) => void;
|
||||
}
|
||||
|
||||
// addHandlerOptions defines the set of possible options that can be provided
|
||||
// to the addHandler function.
|
||||
//
|
||||
// The 'receiveUpdates' option indicates whether the handler can receive
|
||||
// updates and defaults to false. If it is set to false, any queryUpdate
|
||||
// messages that get sent will be discarded. If it is set to 'true', any
|
||||
// queryUpdate messages that get sent will be held until the handler provides a
|
||||
// 'receiveUpdate' function to the ActiveQuery object using the
|
||||
// ActiveQuery.setReceiveUpdate function.
|
||||
interface addHandlerOptions {
|
||||
receiveUpdates?: boolean;
|
||||
}
|
||||
|
||||
// queryRouter defines the hashmap that is used to route queries to their
|
||||
// respective handlers. The 'handler' field is the function that will be called
|
||||
// to process the query, and 'receiveUpdates' is a flag that indicates whether
|
||||
// or not queryUpdate messages should be processed.
|
||||
interface queryRouter {
|
||||
[method: string]: {
|
||||
handler: handlerFn;
|
||||
receiveUpdates: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Set the default handler options so that they can be imported and used by
|
||||
// modules. This is syntactic sugar.
|
||||
const addHandlerDefaultOptions = {
|
||||
receiveUpdates: false,
|
||||
};
|
||||
|
||||
// Create a router which will route methods to their handlers. New handlers can
|
||||
// be added to the router by calling 'addHandler'.
|
||||
//
|
||||
// Currently, there are two default handlers provided by libkmodule. The first
|
||||
// is a handler for 'presentKey', which accepts keys provided by the kernel.
|
||||
// The second is 'no-op', which allows a caller to make a no-op query on the
|
||||
// module, which can be useful both for debugging, and also for 'warming up'
|
||||
// the module so that it's in the kernel cache already the first time that a
|
||||
// user tries to use the module.
|
||||
//
|
||||
// handleMessage implicitly handles 'queryUpdate' and 'responseUpdate' and
|
||||
// 'response' methods as well, but those don't go through the router because
|
||||
// special handling is required for those methods.
|
||||
const router: queryRouter = {};
|
||||
router["presentKey"] = { handler: handlePresentKey, receiveUpdates: false };
|
||||
router["no-op"] = { handler: handleNoOp, receiveUpdates: false };
|
||||
|
||||
// addHandler will add a new handler to the router to process specific methods.
|
||||
//
|
||||
// NOTE: The 'queryUpdate', 'response', and 'responseUpdate' messages are all
|
||||
// handled before the router is considered, and therefore they cannot be
|
||||
// overwritten by calling 'addHandler'.
|
||||
function addHandler(
|
||||
method: string,
|
||||
handler: handlerFn,
|
||||
options?: addHandlerOptions,
|
||||
) {
|
||||
// If options is undefined, use the default options.
|
||||
if (options === undefined) {
|
||||
options = addHandlerDefaultOptions;
|
||||
}
|
||||
|
||||
// Don't set the 'receiveUpdates' flag in the router if the provided
|
||||
// options haven't enabled them.
|
||||
//
|
||||
// NOTE: options.receiveUpdates may be undefined, that's why we
|
||||
// explicitly set it to talse here.
|
||||
if (options.receiveUpdates !== true) {
|
||||
router[method] = { handler, receiveUpdates: false };
|
||||
return;
|
||||
}
|
||||
router[method] = { handler, receiveUpdates: true };
|
||||
}
|
||||
|
||||
// handleMessage is the standard handler for messages. It has special handling
|
||||
// for the 'queryUpdate', 'response', and 'responseUpdate' messages. Otherwise,
|
||||
// it will use the router to connect moduleCalls to the appropriate handler.
|
||||
//
|
||||
// When passing a call off to a handler, it will create an 'ActiveQuery' object
|
||||
// that the handler can work with.
|
||||
function handleMessage(event: MessageEvent) {
|
||||
// Special handling for "response" messages.
|
||||
if (event.data.method === "queryUpdate") {
|
||||
handleQueryUpdate(event);
|
||||
return;
|
||||
}
|
||||
if (event.data.method === "response") {
|
||||
handleResponse(event);
|
||||
return;
|
||||
}
|
||||
if (event.data.method === "responseNonce") {
|
||||
handleResponseNonce(event);
|
||||
return;
|
||||
}
|
||||
if (event.data.method === "responseUpdate") {
|
||||
handleResponseUpdate(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we have a handler for this object.
|
||||
if (!Object.prototype.hasOwnProperty.call(router, event.data.method)) {
|
||||
respondErr(event, "unrecognized method '" + event.data.method + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up the accept and reject functions. They use the 'responded'
|
||||
// variable to ensure that only one response is ever sent.
|
||||
let responded = false;
|
||||
const respond = function (data: any) {
|
||||
// Check if a response was already sent.
|
||||
if (responded) {
|
||||
const str = objAsString(data);
|
||||
logErr("accept called after response already sent: " + str);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a response.
|
||||
responded = true;
|
||||
postMessage({
|
||||
nonce: event.data.nonce,
|
||||
method: "response",
|
||||
err: null,
|
||||
data,
|
||||
});
|
||||
|
||||
// Clear this query from the set of incomingQueries.
|
||||
clearIncomingQuery(event.data.nonce);
|
||||
};
|
||||
const reject = function (err: string) {
|
||||
// Check if a response was already sent.
|
||||
if (responded) {
|
||||
const str = objAsString(err);
|
||||
logErr("reject called after response already sent: " + str);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the response as an error.
|
||||
responded = true;
|
||||
respondErr(event, err);
|
||||
};
|
||||
|
||||
// Define the function that will allow the handler to send an update.
|
||||
const sendUpdate = function (updateData: any) {
|
||||
if (responded) {
|
||||
const str = objAsString(updateData);
|
||||
logErr("sendUpdate called after response already sent: " + str);
|
||||
return;
|
||||
}
|
||||
postMessage({
|
||||
method: "responseUpdate",
|
||||
nonce: event.data.nonce,
|
||||
data: updateData,
|
||||
});
|
||||
};
|
||||
|
||||
// Try to handle the message. If an exception is thrown by the handler,
|
||||
// catch the error and respond with that error.
|
||||
//
|
||||
// NOTE: Throwing exceptions is considered bad practice, this code is only
|
||||
// here because the practice is so common throughout javascript and we want
|
||||
// to make sure developer code works without developers getting too
|
||||
// frustrated.
|
||||
//
|
||||
// NOTE: The final argument contains a set of extra fields about the call,
|
||||
// for example providing the domain of the caller. We used an object for
|
||||
// this final field so that it could be extended later.
|
||||
try {
|
||||
const activeQuery: ActiveQuery = {
|
||||
callerInput: event.data.data,
|
||||
respond,
|
||||
reject,
|
||||
sendUpdate,
|
||||
domain: event.data.domain,
|
||||
};
|
||||
if (router[event.data.method].receiveUpdates) {
|
||||
activeQuery.setReceiveUpdate = getSetReceiveUpdate(event);
|
||||
}
|
||||
router[event.data.method].handler(activeQuery);
|
||||
} catch (err: any) {
|
||||
// Convert the thrown error and log it. We know that strErr is a string
|
||||
// because objAsString must return a string, and addContextToErr only
|
||||
// returns null if strErr is null.
|
||||
const strErr = objAsString(err);
|
||||
const finalErr = <string>addContextToErr(strErr, "module threw an error");
|
||||
logErr(finalErr);
|
||||
|
||||
// Only send a response if a response was not already sent.
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
respondErr(event, finalErr);
|
||||
}
|
||||
}
|
||||
|
||||
// respondErr will send an error to the kernel as a response to a moduleCall.
|
||||
function respondErr(event: MessageEvent, err: string) {
|
||||
const strErr = objAsString(err);
|
||||
postMessage({
|
||||
nonce: event.data.nonce,
|
||||
method: "response",
|
||||
err: strErr,
|
||||
data: null,
|
||||
});
|
||||
clearIncomingQuery(event.data.nonce);
|
||||
}
|
||||
|
||||
export { ActiveQuery, addHandler, DataFn, handleMessage };
|
323
src/queries.ts
323
src/queries.ts
|
@ -1,323 +0,0 @@
|
|||
import { log, logErr } from "./log.js";
|
||||
import { DataFn } from "./messages.js";
|
||||
import { ErrTuple, objAsString } from "@lumeweb/libweb";
|
||||
|
||||
// queryResolve defines the function that gets called to resolve a query. It's
|
||||
// the 'resolve' field of a promise that returns a tuple containing some data
|
||||
// and an err.
|
||||
type queryResolve = (et: ErrTuple) => void;
|
||||
|
||||
// queryMap defines the type for the queries map, which maps a nonce to the
|
||||
// outgoing query that the module made.
|
||||
interface queryMap {
|
||||
[nonce: number]: {
|
||||
resolve: queryResolve;
|
||||
receiveUpdate?: DataFn;
|
||||
kernelNonce?: number;
|
||||
kernelNonceReceived?: DataFn;
|
||||
};
|
||||
}
|
||||
|
||||
// incomingQueryMap defines the type for mapping incoming queries to the method
|
||||
// that can receive queryUpdates. To allow queryUpdate messages to be processed
|
||||
// in the same scope as the original query, we put a 'setReceiveUpdate'
|
||||
// function in the activeQuery object.
|
||||
//
|
||||
// blockForReceiveUpdate is a promise that will be resolved once the
|
||||
// receiveUpdate function has been set.
|
||||
interface incomingQueryMap {
|
||||
[nonce: string]: Promise<DataFn>;
|
||||
}
|
||||
|
||||
// queries is an object that tracks outgoing queries to the kernel. When making
|
||||
// a query, we assign a nonce to that query. All response and responseUpdate
|
||||
// messages for that query will make use of the nonce assigned here. When we
|
||||
// receive a response or responseUpdate message, we will use this map to locate
|
||||
// the original query that is associated with the response.
|
||||
//
|
||||
// The kernel provides security guarantees that all incoming response and
|
||||
// responseUpdate messages have nonces that are associated with the correct
|
||||
// query.
|
||||
//
|
||||
// queries is a hashmap where the nonce is the key and various query state
|
||||
// items are the values.
|
||||
//
|
||||
// NOTE: When sending out queryUpdate messages, the queries need to use the
|
||||
// nonce assigned by the kernel. The nonces in the 'queries' map will not work.
|
||||
let queriesNonce = 0;
|
||||
const queries: queryMap = {};
|
||||
|
||||
// incomingQueries is an object
|
||||
// set of information needed to process queryUpdate messages.
|
||||
const incomingQueries: incomingQueryMap = {};
|
||||
|
||||
// clearIncomingQuery will clear a query with the provided nonce from the set
|
||||
// of incomingQueries. This method gets called when the response is either
|
||||
// accepted or rejected.
|
||||
function clearIncomingQuery(nonce: number) {
|
||||
delete incomingQueries[nonce];
|
||||
}
|
||||
|
||||
// getSetReceiveUpdate returns a function called 'setReceiveUpdate' which can
|
||||
// be called to set the receiveUpdate function for the current query. All
|
||||
// queryUpdate messages that get received will block until setReceiveUpdate has
|
||||
// been called.
|
||||
function getSetReceiveUpdate(
|
||||
event: MessageEvent,
|
||||
): (receiveUpdate: DataFn) => void {
|
||||
// Create the promise that allows us to block until the handler has
|
||||
// provided us its receiveUpdate function.
|
||||
let updateReceived: DataFn;
|
||||
// Add the blockForReceiveUpdate object to the queryUpdateRouter.
|
||||
incomingQueries[event.data.nonce] = new Promise((resolve) => {
|
||||
updateReceived = resolve;
|
||||
});
|
||||
return function (receiveUpdate: DataFn) {
|
||||
updateReceived(receiveUpdate);
|
||||
};
|
||||
}
|
||||
|
||||
// handleQueryUpdate currently discards all queryUpdates.
|
||||
async function handleQueryUpdate(event: MessageEvent) {
|
||||
// Check whether the handler for this query wants to process
|
||||
// receiveUpdate messages. This lookup may also fail if no handler
|
||||
// exists for this nonce, which can happen if the queryUpdate message
|
||||
// created concurrently with a response (which is not considered a bug
|
||||
// or error).
|
||||
if (!(event.data.nonce in incomingQueries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block until the handler has provided a receiveUpdate function, than
|
||||
// call receiveUpdate.
|
||||
const receiveUpdate = await incomingQueries[event.data.nonce];
|
||||
receiveUpdate(event.data.data);
|
||||
}
|
||||
|
||||
// handleResponse will take a response and match it to the correct query.
|
||||
//
|
||||
// NOTE: The kernel guarantees that an err field and a data field and a nonce
|
||||
// field will be present in any message that gets sent using the "response"
|
||||
// method.
|
||||
function handleResponse(event: MessageEvent) {
|
||||
// Look for the query with the corresponding nonce.
|
||||
if (!(event.data.nonce in queries)) {
|
||||
logErr(
|
||||
"no open query found for provided nonce: " + objAsString(event.data.data),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the response is an error.
|
||||
if (event.data.err !== null) {
|
||||
logErr("there's an error in the data");
|
||||
queries[event.data.nonce].resolve([{}, event.data.err]);
|
||||
delete queries[event.data.nonce];
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the handler function using the provided data, then delete the query
|
||||
// from the query map.
|
||||
queries[event.data.nonce].resolve([event.data.data, null]);
|
||||
delete queries[event.data.nonce];
|
||||
}
|
||||
|
||||
// handleResponseNonce will handle a message with the method 'responseNonce'.
|
||||
// This is a message from the kernel which is telling us what nonce we should
|
||||
// use when we send queryUpdate messages to the kernel for a particular query.
|
||||
function handleResponseNonce(event: MessageEvent) {
|
||||
// Check if the query exists. If it does not exist, it's possible that the
|
||||
// messages just arrived out of order and nothing is going wrong.
|
||||
if (!(event.data.nonce in queries)) {
|
||||
logErr("temp err: nonce could not be found");
|
||||
return;
|
||||
}
|
||||
const query = queries[event.data.nonce];
|
||||
if ("kernelNonce" in query) {
|
||||
logErr("received two responseNonce messages for the same query nonce");
|
||||
return;
|
||||
}
|
||||
if (typeof query["kernelNonceReceived"] !== "function") {
|
||||
// We got a nonce even though one wasn't requested.
|
||||
log("received nonce even though none was requested");
|
||||
return;
|
||||
}
|
||||
query["kernelNonce"] = event.data.data.nonce;
|
||||
query.kernelNonceReceived();
|
||||
return;
|
||||
}
|
||||
|
||||
// handleResponseUpdate attempts to find the corresponding query using the
|
||||
// nonce and then calls the corresponding receiveUpdate function.
|
||||
//
|
||||
// Because response and responseUpdate messages are sent asynchronously, it's
|
||||
// completely possible that a responseUpdate is received after the query has
|
||||
// been closed out by a response. We therefore just ignore any messages that
|
||||
// can't be matched to a nonce.
|
||||
function handleResponseUpdate(event: MessageEvent) {
|
||||
// Ignore this message if there is no corresponding query, the query may
|
||||
// have been closed out and this message was just processed late.
|
||||
if (!(event.data.nonce in queries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check whether a receiveUpdate function was set, and if so pass the
|
||||
// update along. To prevent typescript
|
||||
const query = queries[event.data.nonce];
|
||||
if (typeof query["receiveUpdate"] === "function") {
|
||||
query.receiveUpdate(event.data.data);
|
||||
}
|
||||
}
|
||||
|
||||
// callModule is a generic function to call a module. It will return whatever
|
||||
// response is provided by the module.
|
||||
//
|
||||
// callModule can only be used for query-response communications, there is no
|
||||
// support for handling queryUpdate or responseUpdate messages - they will be
|
||||
// ignored if received. If you need those messages, use 'connectModule'
|
||||
// instead.
|
||||
function callModule(
|
||||
module: string,
|
||||
method: string,
|
||||
data?: any,
|
||||
): Promise<ErrTuple> {
|
||||
const moduleCallData = {
|
||||
module,
|
||||
method,
|
||||
data,
|
||||
};
|
||||
const [, query] = newKernelQuery("moduleCall", moduleCallData, false);
|
||||
return query;
|
||||
}
|
||||
|
||||
// connectModule is a generic function to connect to a module. It is similar to
|
||||
// callModule, except that it also supports sending and receiving updates in
|
||||
// the middule of the call. If the module being called sends and update, the
|
||||
// updated will be passed to the caller through the 'receiveUpdate' function.
|
||||
// If the caller wishes to send an update to the module, it can use the
|
||||
// provided 'sendUpdate' function.
|
||||
//
|
||||
// The call signature is a bit messy, so let's disect it a bit. The input
|
||||
// values are the same as callModule, except there's a fourth input for
|
||||
// providing a 'receiveUpdate' function. It is okay to provide 'null' or
|
||||
// 'undefined' as the function to receive updates if you do not care to receive
|
||||
// or process any updates sent by the module. If you do want to receive
|
||||
// updates, the receiveUpdate function should have the following function
|
||||
// signature:
|
||||
//
|
||||
// `function receiveUpdate(data: any)`
|
||||
//
|
||||
// The data that gets sent is at the full discretion of the module, and will
|
||||
// depend on which method was called in the original query.
|
||||
//
|
||||
// The return value is a tuple of a 'sendUpdate' function and a promise. The
|
||||
// promise itself resolves to a tuple which matches the tuple in the
|
||||
// 'callModule' function - the first value is the response data, and the second
|
||||
// value is an error. When the promise resolves, it means the query has
|
||||
// completed and no more updates will be processed. Therefore, 'sendUpdate' is
|
||||
// only valid until the promise resolves.
|
||||
//
|
||||
// sendUpdate has the following function signature:
|
||||
//
|
||||
// `function sendUpdate(data: any)`
|
||||
//
|
||||
// Like 'receiveUpdate', the data that should be sent when sending an update to
|
||||
// the module is entirely determined by the module and will vary based on what
|
||||
// method was called in the original query.
|
||||
function connectModule(
|
||||
module: string,
|
||||
method: string,
|
||||
data: any,
|
||||
receiveUpdate: DataFn,
|
||||
): [sendUpdate: DataFn, response: Promise<ErrTuple>] {
|
||||
const moduleCallData = {
|
||||
module,
|
||||
method,
|
||||
data,
|
||||
};
|
||||
// We omit the 'receiveUpdate' function because this is a no-op. If the
|
||||
// value is not defined, newKernelQuery will place in a no-op for us.
|
||||
return newKernelQuery("moduleCall", moduleCallData, true, receiveUpdate);
|
||||
}
|
||||
|
||||
// newKernelQuery creates a query to send to the kernel. It automatically
|
||||
// handles details like the nonces and the postMessage communicaations.
|
||||
//
|
||||
// The first input is the method being called on the kernel, and the second
|
||||
// input is the data being provided as input to the method.
|
||||
//
|
||||
// The third input is a boolean indicating whether or not you want to send
|
||||
// queryUpdates. There is a small amount of performance overhead associated
|
||||
// with sending updates (slightly under one millisecond) so we only set up the
|
||||
// ability to send updates if it is requested.
|
||||
//
|
||||
// The final input is an optional function that gets called when a
|
||||
// responseUpdate is received. If no fourth input is provided, responseUpdates
|
||||
// will be ignored.
|
||||
//
|
||||
// NOTE: Typically developers should not use this function. Instead use
|
||||
// 'callModule' or 'connectModule'.
|
||||
function newKernelQuery(
|
||||
method: string,
|
||||
data: any,
|
||||
sendUpdates: boolean,
|
||||
receiveUpdate?: DataFn,
|
||||
): [sendUpdate: DataFn, response: Promise<ErrTuple>] {
|
||||
// Get the nonce for the query.
|
||||
const nonce = queriesNonce;
|
||||
queriesNonce += 1;
|
||||
|
||||
// Create the sendUpdate function, which allows the caller to send a
|
||||
// queryUpdate. The update cannot actually be sent until the kernel has told us the responseNonce
|
||||
let sendUpdate: DataFn = () => {};
|
||||
|
||||
// Establish the query in the queries map and then send the query to the
|
||||
// kernel.
|
||||
const p: Promise<ErrTuple> = new Promise((resolve) => {
|
||||
queries[nonce] = { resolve };
|
||||
});
|
||||
if (receiveUpdate !== null && receiveUpdate !== undefined) {
|
||||
queries[nonce]["receiveUpdate"] = receiveUpdate;
|
||||
}
|
||||
if (sendUpdates) {
|
||||
// Set up the promise that resovles when we have received the responseNonce
|
||||
// from the kernel.
|
||||
const blockForKernelNonce = new Promise((resolve) => {
|
||||
queries[nonce]["kernelNonceReceived"] = resolve;
|
||||
});
|
||||
sendUpdate = function (updateData: any) {
|
||||
blockForKernelNonce.then(() => {
|
||||
// It's possible that the query was already closed and deleted from
|
||||
// the queries map, so we need an existence check before completing
|
||||
// the postMessage call.
|
||||
if (!(nonce in queries)) {
|
||||
return;
|
||||
}
|
||||
postMessage({
|
||||
method: "queryUpdate",
|
||||
nonce: queries[nonce].kernelNonce,
|
||||
data: updateData,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
postMessage({
|
||||
method,
|
||||
nonce,
|
||||
data,
|
||||
sendKernelNonce: sendUpdates,
|
||||
});
|
||||
return [sendUpdate, p];
|
||||
}
|
||||
|
||||
export {
|
||||
callModule,
|
||||
clearIncomingQuery,
|
||||
connectModule,
|
||||
getSetReceiveUpdate,
|
||||
handleQueryUpdate,
|
||||
handleResponse,
|
||||
handleResponseNonce,
|
||||
handleResponseUpdate,
|
||||
newKernelQuery,
|
||||
};
|
40
src/types.ts
40
src/types.ts
|
@ -1,40 +0,0 @@
|
|||
// moduleQuery defines a query that can be sent to a module. The method is used
|
||||
// to tell the module what query is being made. The domain is set by the
|
||||
// kernel, and is guaranteed to match the domain of the caller. The module can
|
||||
// use the 'domain' to enforce access control policies. The 'data' can be any
|
||||
// arbitrary object, and will depend on the method. The module developer is
|
||||
// ultimately the one who decides what data should be provided as input to each
|
||||
// method call.
|
||||
//
|
||||
// NOTE: While the kernel does do verification for the method and domain, the
|
||||
// kernel does not do any verification for the data field. The module itself is
|
||||
// responsible for verifying all inputs provided in the data field.
|
||||
interface moduleQuery {
|
||||
method: string;
|
||||
domain: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
// presentKeyData contains the data that gets sent in a 'presentKey' call
|
||||
// from the kernel. 'presentKey' is called on the module immediately after the
|
||||
// module starts up.
|
||||
//
|
||||
// The 'key' is a unique key dervied by the kernel for the module based on
|
||||
// the module's domain and the key of the user. Modules in different domains
|
||||
// will have different keys, and have no way to guess what the keys of other
|
||||
// modules are.
|
||||
//
|
||||
// It is safe to use the 'key' for things like blockchain wallets.
|
||||
//
|
||||
// If the module has been given access to the root private key,
|
||||
// presentKeyData will include the rootPrivateKey. If the module does not
|
||||
// have access to the root private key, the field will not be included. A
|
||||
// module that receives the root private key has full read and write access
|
||||
// to all of the user's data.
|
||||
//
|
||||
interface presentKeyData {
|
||||
key: Uint8Array;
|
||||
rootPrivateKey?: Uint8Array;
|
||||
}
|
||||
|
||||
export { moduleQuery, presentKeyData };
|
Loading…
Reference in New Issue