TS migration: Convert `localstorage.js` to `localstorage.ts`.

Added function parameter types, return type, and types of local
varaibles. Added a `null` check for `raw_data` before `JSON.parse`.
Created a type `FormData` and an export type `LocalStorage` to
imporve conciseness and clearity.

Type `LocalStorage` is exported because it might be used in other
files based on an observation that many files have imported `localstorage`.
This commit is contained in:
AcKindle3 2023-03-25 11:54:49 -04:00 committed by Tim Abbott
parent 3e6f34780a
commit 70b8eb3423
2 changed files with 63 additions and 20 deletions

View File

@ -104,7 +104,7 @@ EXEMPT_FILES = make_set(
"web/src/list_widget.js", "web/src/list_widget.js",
"web/src/loading.ts", "web/src/loading.ts",
"web/src/local_message.js", "web/src/local_message.js",
"web/src/localstorage.js", "web/src/localstorage.ts",
"web/src/message_edit.js", "web/src/message_edit.js",
"web/src/message_edit_history.js", "web/src/message_edit_history.js",
"web/src/message_events.js", "web/src/message_events.js",

View File

@ -1,19 +1,42 @@
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
type FormData = {
data: unknown;
__valid: true;
expires: number | null;
};
export type LocalStorage = {
setExpiry(expires: number, isGlobal: boolean): LocalStorage;
get(name: string): unknown;
set(name: string, data: unknown): boolean;
remove(name: string): void;
removeDataRegexWithCondition(
name: string,
condition_checker: (value: string | null | undefined) => boolean,
): void;
migrate<T = unknown>(
name: string,
v1: number,
v2: number,
callback: (data: unknown) => T,
): T | undefined;
};
const ls = { const ls = {
// check if the datestamp is from before now and if so return true. // check if the datestamp is from before now and if so return true.
isExpired(stamp) { isExpired(stamp: number): boolean {
return new Date(stamp) < new Date(); return new Date(stamp) < new Date();
}, },
// return the localStorage key that is bound to a version of a key. // return the localStorage key that is bound to a version of a key.
formGetter(version, name) { formGetter(version: number, name: string): string {
return "ls__" + version + "__" + name; return `ls__${version}__${name}`;
}, },
// create a formData object to put in the data, a signature that it was // create a formData object to put in the data, a signature that it was
// created with this library, and when it expires (if ever). // created with this library, and when it expires (if ever).
formData(data, expires) { formData(data: unknown, expires: number): FormData {
return { return {
data, data,
__valid: true, __valid: true,
@ -21,11 +44,14 @@ const ls = {
}; };
}, },
getData(version, name) { getData(version: number, name: string): FormData | undefined {
const key = this.formGetter(version, name); const key = this.formGetter(version, name);
let data; let data;
try { try {
const raw_data = localStorage.getItem(key); const raw_data = localStorage.getItem(key);
if (raw_data === null) {
return undefined;
}
data = JSON.parse(raw_data); data = JSON.parse(raw_data);
} catch { } catch {
// data stays undefined // data stays undefined
@ -35,7 +61,7 @@ const ls = {
data.__valid && data.__valid &&
// JSON forms of data with `Infinity` turns into `null`, // JSON forms of data with `Infinity` turns into `null`,
// so if null then it hasn't expired since nothing was specified. // so if null then it hasn't expired since nothing was specified.
(!ls.isExpired(data.expires) || data.expires === null) (data.expires === null || !ls.isExpired(data.expires))
) { ) {
return data; return data;
} }
@ -44,7 +70,7 @@ const ls = {
}, },
// set the wrapped version of the data into localStorage. // set the wrapped version of the data into localStorage.
setData(version, name, data, expires) { setData(version: number, name: string, data: unknown, expires: number): void {
const key = this.formGetter(version, name); const key = this.formGetter(version, name);
const val = this.formData(data, expires); const val = this.formData(data, expires);
@ -52,7 +78,7 @@ const ls = {
}, },
// remove the key from localStorage and from memory. // remove the key from localStorage and from memory.
removeData(version, name) { removeData(version: number, name: string): void {
const key = this.formGetter(version, name); const key = this.formGetter(version, name);
localStorage.removeItem(key); localStorage.removeItem(key);
@ -61,9 +87,13 @@ const ls = {
// Remove keys which (1) map to a value that satisfies a // Remove keys which (1) map to a value that satisfies a
// property tested by `condition_checker` and (2) which match // property tested by `condition_checker` and (2) which match
// the pattern given by `name`. // the pattern given by `name`.
removeDataRegexWithCondition(version, regex, condition_checker) { removeDataRegexWithCondition(
version: number,
regex: string,
condition_checker: (value: string | null | undefined) => boolean,
): void {
const key_regex = new RegExp(this.formGetter(version, regex)); const key_regex = new RegExp(this.formGetter(version, regex));
let keys = []; let keys: string[] = [];
try { try {
keys = Object.keys(localStorage); keys = Object.keys(localStorage);
} catch { } catch {
@ -93,7 +123,12 @@ const ls = {
// migrate from an older version of a data src to a newer one with a // migrate from an older version of a data src to a newer one with a
// specified callback function. // specified callback function.
migrate(name, v1, v2, callback) { migrate<T = unknown>(
name: string,
v1: number,
v2: number,
callback: (oldData: unknown) => T,
): T | undefined {
const old = this.getData(v1, name); const old = this.getData(v1, name);
this.removeData(v1, name); this.removeData(v1, name);
@ -109,7 +144,7 @@ const ls = {
}; };
// return a new function instance that has instance-scoped variables. // return a new function instance that has instance-scoped variables.
export const localstorage = function () { export const localstorage = function (): LocalStorage {
const _data = { const _data = {
VERSION: 1, VERSION: 1,
expires: Number.POSITIVE_INFINITY, expires: Number.POSITIVE_INFINITY,
@ -120,14 +155,14 @@ export const localstorage = function () {
// `expires` should be a Number that represents the number of ms from // `expires` should be a Number that represents the number of ms from
// now that this should expire in. // now that this should expire in.
// this allows for it to either be set only once or permanently. // this allows for it to either be set only once or permanently.
setExpiry(expires, isGlobal) { setExpiry(expires: number, isGlobal: boolean): LocalStorage {
_data.expires = expires; _data.expires = expires;
_data.expiresIsGlobal = isGlobal || false; _data.expiresIsGlobal = isGlobal || false;
return this; return this;
}, },
get(name) { get(name: string): unknown {
const data = ls.getData(_data.VERSION, name); const data = ls.getData(_data.VERSION, name);
if (data) { if (data) {
@ -137,7 +172,7 @@ export const localstorage = function () {
return undefined; return undefined;
}, },
set(name, data) { set(name: string, data: unknown): boolean {
if (_data.VERSION !== undefined) { if (_data.VERSION !== undefined) {
ls.setData(_data.VERSION, name, data, _data.expires); ls.setData(_data.VERSION, name, data, _data.expires);
@ -155,18 +190,26 @@ export const localstorage = function () {
}, },
// remove a key with a given version. // remove a key with a given version.
remove(name) { remove(name: string): void {
ls.removeData(_data.VERSION, name); ls.removeData(_data.VERSION, name);
}, },
// Remove keys which (1) map to a value that satisfies a // Remove keys which (1) map to a value that satisfies a
// property tested by `condition_checker` AND (2) which // property tested by `condition_checker` AND (2) which
// match the pattern given by `name`. // match the pattern given by `name`.
removeDataRegexWithCondition(name, condition_checker) { removeDataRegexWithCondition(
name: string,
condition_checker: (value: string | null | undefined) => boolean,
): void {
ls.removeDataRegexWithCondition(_data.VERSION, name, condition_checker); ls.removeDataRegexWithCondition(_data.VERSION, name, condition_checker);
}, },
migrate(name, v1, v2, callback) { migrate<T = unknown>(
name: string,
v1: number,
v2: number,
callback: (data: unknown) => T,
): T | undefined {
return ls.migrate(name, v1, v2, callback); return ls.migrate(name, v1, v2, callback);
}, },
}; };
@ -186,7 +229,7 @@ export const localstorage = function () {
let warned_of_localstorage = false; let warned_of_localstorage = false;
localstorage.supported = function supports_localstorage() { localstorage.supported = function supports_localstorage(): boolean {
try { try {
return window.localStorage !== undefined && window.localStorage !== null; return window.localStorage !== undefined && window.localStorage !== null;
} catch { } catch {