storage api + error boundary

This commit is contained in:
Jelle van Snik 2022-02-16 21:30:12 +01:00
parent f1ffa98a2b
commit afb6958995
10 changed files with 378 additions and 15 deletions

View file

@ -16,6 +16,8 @@
"react-helmet": "^6.1.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^5.0.0",
"react-tracked": "^1.7.6",
"scheduler": "^0.20.2",
"web-vitals": "^1.0.1"
},
"scripts": {

View file

@ -0,0 +1,64 @@
import { Title } from "components/Text/Title";
import { Component } from "react";
interface ErrorBoundaryState {
hasError: boolean;
error?: {
name: string;
description: string;
path: string;
};
}
export class ErrorBoundary extends Component<{}, ErrorBoundaryState> {
state: ErrorBoundaryState = {
hasError: false,
};
static getDerivedStateFromError() {
return {
hasError: true,
};
}
componentDidCatch(error: any, errorInfo: any) {
console.error("Render error caught", error, errorInfo);
if (error instanceof Error) {
let realError: Error = error as Error;
this.setState((s) => ({
...s,
hasError: true,
error: {
name: realError.name,
description: realError.message,
path: errorInfo.componentStack.split("\n")[1],
},
}));
}
}
render() {
if (!this.state.hasError) return this.props.children;
// TODO make pretty
return (
<div>
<div>
<Title>Whoops, it broke</Title>
<p>
The app encountered an error and wasn't able to recover, please
report it to the discord server or on GitHub.
</p>
</div>
{this.state.error ? (
<div>
<p className="txt-white">
{this.state.error.name} - {this.state.error.description}
</p>
<p>{this.state.error.path}</p>
</div>
) : null}
</div>
);
}
}

View file

@ -4,5 +4,5 @@
html,
body {
@apply min-h-screen bg-denim-100 text-white font-open-sans;
@apply bg-denim-100 text-denim-700 font-open-sans min-h-screen;
}

View file

@ -1,14 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import React from "react";
import ReactDOM from "react-dom";
import { HashRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import { ErrorBoundary } from "components/layout/ErrorBoundary";
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary>
<HashRouter>
<App />
</HashRouter>
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root')
document.getElementById("root")
);

View file

@ -0,0 +1,50 @@
import { versionedStoreBuilder } from 'utils/storage';
/*
version 0
{
[{scraperid}]: {
movie: {
[{movie-id}]: {
full: {
currentlyAt: number,
totalDuration: number,
updatedAt: number, // unix timestamp in ms
meta: FullMetaObject, // no idea whats in here
}
}
},
show: {
[{show-id}]: {
[{season}-{episode}]: {
currentlyAt: number,
totalDuration: number,
updatedAt: number, // unix timestamp in ms
show: {
episode: string,
season: string,
},
meta: FullMetaObject, // no idea whats in here
}
}
}
}
}
*/
export const VideoProgressStore = versionedStoreBuilder()
.setKey('video-progress')
.addVersion({
version: 0,
migrate(data: any) {
// TODO migration
throw new Error("Migration not been written yet!!")
},
})
.addVersion({
version: 1,
create() {
return {}
}
})
.build()

226
src/utils/storage.ts Normal file
View file

@ -0,0 +1,226 @@
// TODO make type and react safe!!
/*
it needs to be react-ified by having a save function not on the instance itself.
also type safety is important, this is all spaghetti with "any" everywhere
*/
function buildStoreObject(d: any) {
const data: any = {
versions: d.versions,
currentVersion: d.maxVersion,
id: d.storageString,
};
function update(this: any, obj: any) {
if (!obj) throw new Error("object to update is not an object");
// repeat until object fully updated
if (obj["--version"] === undefined) obj["--version"] = 0;
while (obj["--version"] !== this.currentVersion) {
// get version
let version: any = obj["--version"] || 0;
if (version.constructor !== Number || version < 0) version = -42;
// invalid on purpose so it will reset
else {
version = (version as number + 1).toString();
}
// check if version exists
if (!this.versions[version]) {
console.error(
`Version not found for storage item in store ${this.id}, resetting`
);
obj = null;
break;
}
// update object
obj = this.versions[version].update(obj);
}
// if resulting obj is null, use latest version as init object
if (obj === null) {
console.error(
`Storage item for store ${this.id} has been reset due to faulty updates`
);
return this.versions[this.currentVersion.toString()].init();
}
// updates succesful, return
return obj;
}
function get(this: any) {
// get from storage api
const store = this;
let data: any = localStorage.getItem(this.id);
// parse json if item exists
if (data) {
try {
data = JSON.parse(data);
if (!data.constructor) {
console.error(
`Storage item for store ${this.id} has not constructor`
);
throw new Error("storage item has no constructor");
}
if (data.constructor !== Object) {
console.error(`Storage item for store ${this.id} is not an object`);
throw new Error("storage item is not an object");
}
} catch (_) {
// if errored, set to null so it generates new one, see below
console.error(`Failed to parse storage item for store ${this.id}`);
data = null;
}
}
// if item doesnt exist, generate from version init
if (!data) {
data = this.versions[this.currentVersion.toString()].init();
}
// update the data if needed
data = this.update(data);
// add a save object to return value
data.save = function save() {
localStorage.setItem(store.id, JSON.stringify(data));
};
// add instance helpers
Object.entries(d.instanceHelpers).forEach(([name, helper]: any) => {
if (data[name] !== undefined)
throw new Error(
`helper name: ${name} on instance of store ${this.id} is reserved`
);
data[name] = helper.bind(data);
});
// return data
return data;
}
// add functions to store
data.get = get.bind(data);
data.update = update.bind(data);
// add static helpers
Object.entries(d.staticHelpers).forEach(([name, helper]: any) => {
if (data[name] !== undefined)
throw new Error(`helper name: ${name} on store ${data.id} is reserved`);
data[name] = helper.bind({});
});
return data;
}
/*
* Builds a versioned store
*
* manages versioning of localstorage items
*/
export function versionedStoreBuilder(): any {
return {
_data: {
versionList: [],
maxVersion: 0,
versions: {},
storageString: undefined,
instanceHelpers: {},
staticHelpers: {},
},
setKey(str: string) {
this._data.storageString = str;
return this;
},
addVersion({ version, migrate, create }: any) {
// input checking
if (version < 0) throw new Error("Cannot add version below 0 in store");
if (version > 0 && !migrate)
throw new Error(
`Missing migration on version ${version} (needed for any version above 0)`
);
// update max version list
if (version > this._data.maxVersion) this._data.maxVersion = version;
// add to version list
this._data.versionList.push(version);
// register version
this._data.versions[version.toString()] = {
version: version, // version number
update: migrate
? (data: any) => {
// update function, and increment version
migrate(data);
data["--version"] = version;
return data;
}
: undefined,
init: create
? () => {
// return an initial object
const data = create();
data["--version"] = version;
return data;
}
: undefined,
};
return this;
},
registerHelper({ name, helper, type }: any) {
// type
if (!type) type = "instance";
// input checking
if (!name || name.constructor !== String) {
throw new Error("helper name is not a string");
}
if (!helper || helper.constructor !== Function) {
throw new Error("helper function is not a function");
}
if (!["instance", "static"].includes(type)) {
throw new Error("helper type must be either 'instance' or 'static'");
}
// register helper
if (type === "instance") this._data.instanceHelpers[name as string] = helper;
else if (type === "static") this._data.staticHelpers[name as string] = helper;
return this;
},
build() {
// check if version list doesnt skip versions
const versionListSorted = this._data.versionList.sort((a: number, b: number) => a - b);
versionListSorted.forEach((v: any, i: number, arr: any[]) => {
if (i === 0) return;
if (v !== arr[i - 1] + 1)
throw new Error("Version list of store is not incremental");
});
// version zero must exist
if (versionListSorted[0] !== 0)
throw new Error("Version 0 doesn't exist in version list of store");
// max version must have init function
if (!this._data.versions[this._data.maxVersion.toString()].init)
throw new Error(
`Missing create function on version ${this._data.maxVersion} (needed for latest version of store)`
);
// check storage string
if (!this._data.storageString)
throw new Error("storage key not set in store");
// build versioned store
return buildStoreObject(this._data);
},
};
}

View file

@ -3,5 +3,5 @@ export function MovieView() {
<div>
<p>Movie view here</p>
</div>
)
);
}

View file

@ -50,7 +50,7 @@ export function SearchView() {
))}
</SectionHeading>
) : null}
{search.searchQuery !== "" && results.length == 0 ? <Loading /> : null}
{search.searchQuery !== "" && results.length === 0 ? <Loading /> : null}
</ThinContainer>
);
}

View file

@ -7255,6 +7255,11 @@ proxy-addr@~2.0.5:
forwarded "0.2.0"
ipaddr.js "1.9.1"
proxy-compare@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-2.0.2.tgz#343e624d0ec399dfbe575f1d365d4fa042c9fc69"
integrity sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A==
psl@^1.1.33:
version "1.8.0"
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz"
@ -7489,6 +7494,14 @@ react-side-effect@^2.1.0:
resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.1.tgz"
integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==
react-tracked@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/react-tracked/-/react-tracked-1.7.6.tgz#11bccec80acccdf5029db20171a887b8b16b7ae1"
integrity sha512-yqfkqj4UZpsadBLIHnPLrc8a0SLgjKoSQrdyipfWeXvLnPl+/AV8MrqRVbNogUJsqHOo+ojlWy2PMxuZPVcPnQ==
dependencies:
proxy-compare "2.0.2"
use-context-selector "1.3.9"
react@^17.0.2:
version "17.0.2"
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
@ -7820,7 +7833,7 @@ saxes@^5.0.1:
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies:
loose-envify "^1.1.0"
@ -8719,6 +8732,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
use-context-selector@1.3.9:
version "1.3.9"
resolved "https://registry.yarnpkg.com/use-context-selector/-/use-context-selector-1.3.9.tgz#d1527393839f0d790ccdd52e28e8f353b8be6c2e"
integrity sha512-YgzRyeFjoJXwFn2qLVAuIbV6EQ8DOuzu3SS/eiCxyAyvBhcn02jYSz8c5v22QQU3LW6Ez/Iyo62kKvS7Kdqt3A==
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"