From 44349bef9cf3a8f5005760384df36eb17bfc875e Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Thu, 7 Sep 2023 19:41:11 -0400 Subject: [PATCH] refactor: moving types and functions from libweb --- src/errTracker.ts | 101 ++++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 87 +++++++++++++++++++++++++++++++++++++++ src/util.ts | 78 +++++++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 src/errTracker.ts create mode 100644 src/types.ts create mode 100644 src/util.ts diff --git a/src/errTracker.ts b/src/errTracker.ts new file mode 100644 index 0000000..bbec1c2 --- /dev/null +++ b/src/errTracker.ts @@ -0,0 +1,101 @@ +// errTracker.ts defines an 'ErrTracker' type which keeps track of historical +// errors. When the number of errors gets too large, it randomly starts pruning +// errors. It always keeps 250 of the most recent errors, and then keeps up to +// 500 historic errors, where the first few errors after runtime are always +// kept, and the ones in the middle are increasingly likely to be omitted from +// the history. + +import { Err } from "./types.js"; + +// MAX_ERRORS defines the maximum number of errors that will be held in the +// HistoricErr object. +const MAX_ERRORS = 1000; + +// HistoricErr is a wrapper that adds a date to the Err type. +interface HistoricErr { + err: Err; + date: Date; +} + +// ErrTracker keeps track of errors that have happened, randomly dropping +// errors to prevent the tracker from using too much memory if there happen to +// be a large number of errors. +interface ErrTracker { + recentErrs: HistoricErr[]; + oldErrs: HistoricErr[]; + + addErr: (err: Err) => void; + viewErrs: () => HistoricErr[]; +} + +// newErrTracker returns an ErrTracker object that is ready to have errors +// added to it. +function newErrTracker(): ErrTracker { + const et: ErrTracker = { + recentErrs: [], + oldErrs: [], + + addErr: function (err: Err): void { + addHistoricErr(et, err); + }, + viewErrs: function (): HistoricErr[] { + return viewErrs(et); + }, + }; + return et; +} + +// addHistoricErr is a function that will add an error to a set of historic +// errors. It uses randomness to prune errors once the error object is too +// large. +function addHistoricErr(et: ErrTracker, err: Err): void { + // Add this error to the set of most recent errors. + et.recentErrs.push({ + err, + date: new Date(), + }); + + // Determine whether some of the most recent errors need to be moved into + // logTermErrs. If the length of the mostRecentErrs is not at least half of + // the MAX_ERRORS, we don't need to do anything. + if (et.recentErrs.length < MAX_ERRORS / 2) { + return; + } + + // Iterate through the recentErrs. For the first half of the recentErrs, we + // will use randomness to either toss them or move them to oldErrs. The + // second half of the recentErrs will be kept as the new recentErrs array. + const newRecentErrs : HistoricErr[] = []; + for (let i = 0; i < et.recentErrs.length; i++) { + // If we are in the second half of the array, add the element to + // newRecentErrs. + if (i > et.recentErrs.length / 2) { + newRecentErrs.push(et.recentErrs[i]); + continue; + } + + // We are in the first half of the array, use a random number to add the + // error oldErrs probabilistically. + const rand = Math.random(); + const target = et.oldErrs.length / (MAX_ERRORS / 2); + if (rand > target || et.oldErrs.length < 25) { + et.oldErrs.push(et.recentErrs[i]); + } + } + et.recentErrs = newRecentErrs; +} + +// viewErrs returns the list of errors that have been retained by the +// HistoricErr object. +function viewErrs(et: ErrTracker): HistoricErr[] { + const finalErrs: HistoricErr[] = []; + for (let i = 0; i < et.oldErrs.length; i++) { + finalErrs.push(et.oldErrs[i]); + } + for (let i = 0; i < et.recentErrs.length; i++) { + finalErrs.push(et.recentErrs[i]); + } + return finalErrs; +} + +export { ErrTracker, HistoricErr, newErrTracker }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..206804c --- /dev/null +++ b/src/types.ts @@ -0,0 +1,87 @@ +// DataFn can take any object as input and has no return value. The input is +// allowed to be undefined. +type DataFn = (data?: any) => void; + +// Err is an error type that is either a string or a null. If the value is +// null, that indicates that there was no error. If the value is a string, it +// indicates that there was an error and the string contains the error message. +// +// The skynet libraries prefer this error type to the standard Error type +// because many times skynet libraries need to pass errors over postMessage, +// and the 'Error' type is not able to be sent over postMessage. +type Err = string | null; + +// ErrFn must take an error message as input. The input is not allowed to be +// undefined or null, there must be an error. +type ErrFn = (errMsg: string) => void; + +// ErrTuple is a type that pairs a 'data' field with an 'err' field. Skynet +// libraries typically prefer returning ErrTuples to throwing or rejecting, +// because it allows upstream code to avoid the try/catch/throw pattern. Though +// the pattern is much celebrated in javascript, it encourages relaxed error +// handling, and often makes error handling much more difficult because the try +// and the catch are in different scopes. +// +// Most of the Skynet core libraries do not have any `throws` anywhere in their +// API. +// +// Typically, an ErrTuple will have only one field filled out. If data is +// returned, the err should be 'null'. If an error is returned, the data field +// should generally be empty. Callers are expected to check the error before +// they access any part of the data field. +type ErrTuple = [data: T, err: Err]; + +// KernelAuthStatus is the structure of a message that gets sent by the kernel +// containing its auth status. Auth occurs in 5 stages. +// +// Stage 0; no auth updates +// Stage 1: bootloader is loaded, user is not yet logged in +// Stage 2: bootloader is loaded, user is logged in +// Stage 3: kernel is loaded, user is logged in +// Stage 4: kernel is loaded, user is logging out (refresh iminent) +// +// 'kernelLoaded' is initially set to "not yet" and will be updated when the +// kernel has loaded. If it is set to "success", it means the kernel loaded +// without issues. If it is set to anything else, it means that there was an +// error, and the new value is the error. +// +// 'kernelLoaded' will not be changed until 'loginComplete' has been set to +// true. 'loginComplete' can be set to true immediately if the user is already +// logged in. +// +// 'logoutComplete' can be set to 'true' at any point, which indicates that the +// auth cycle needs to reset. +interface KernelAuthStatus { + loginComplete: boolean; + kernelLoaded: "not yet" | "success" | string; + logoutComplete: boolean; +} + +interface Portal { + id: string; + name: string; + url: string; +} + +// RequestOverrideResponse defines the type that the kernel returns as a +// response to a requestOverride call. +interface RequestOverrideResponse { + override: boolean; + headers?: any; // TODO: I don't know how to do an array of types. + body?: Uint8Array; +} + +export interface KeyPair { + publicKey: Uint8Array; + privateKey: Uint8Array; +} + +export { + DataFn, + ErrFn, + Err, + ErrTuple, + KernelAuthStatus, + RequestOverrideResponse, + Portal, +}; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..d70aa75 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,78 @@ +// addContextToErr is a helper function that standardizes the formatting of +// adding context to an error. +// +// NOTE: To protect against accidental situations where an Error type or some +// other type is provided instead of a string, we wrap both of the inputs with +// objAsString before returning them. This prevents runtime failures. +function addContextToErr(err: any, context: string): string { + if (err === null || err === undefined) { + err = "[no error provided]"; + } + return objAsString(context) + ": " + objAsString(err); +} + +// objAsString will try to return the provided object as a string. If the +// object is already a string, it will be returned without modification. If the +// object is an 'Error', the message of the error will be returned. If the object +// has a toString method, the toString method will be called and the result +// will be returned. If the object is null or undefined, a special string will +// be returned indicating that the undefined/null object cannot be converted to +// a string. In all other cases, JSON.stringify is used. If JSON.stringify +// throws an exception, a message "[could not provide object as string]" will +// be returned. +// +// NOTE: objAsString is intended to produce human readable output. It is lossy, +// and it is not intended to be used for serialization. +function objAsString(obj: any): string { + // Check for undefined input. + if (obj === undefined) { + return "[cannot convert undefined to string]"; + } + if (obj === null) { + return "[cannot convert null to string]"; + } + + // Parse the error into a string. + if (typeof obj === "string") { + return obj; + } + + // Check if the object is an error, and return the message of the error if + // so. + if (obj instanceof Error) { + return obj.message; + } + + // Check if the object has a 'toString' method defined on it. To ensure + // that we don't crash or throw, check that the toString is a function, and + // also that the return value of toString is a string. + if (Object.prototype.hasOwnProperty.call(obj, "toString")) { + if (typeof obj.toString === "function") { + const str = obj.toString(); + if (typeof str === "string") { + return str; + } + } + } + + // If the object does not have a custom toString, attempt to perform a + // JSON.stringify. We use a lot of bigints in libskynet, and calling + // JSON.stringify on an object with a bigint will cause a throw, so we add + // some custom handling to allow bigint objects to still be encoded. + try { + return JSON.stringify(obj, (_, v) => { + if (typeof v === "bigint") { + return v.toString(); + } + return v; + }); + } catch (err: any) { + if (err !== undefined && typeof err.message === "string") { + return `[stringify failed]: ${err.message}`; + } + return "[stringify failed]"; + } +} + +export { objAsString }; +export { addContextToErr };