Rewrite Redirect IDs to be numeric instead of UUIDs.

This commit is contained in:
Bauke 2022-10-27 21:13:02 +02:00
parent f870c0bbf6
commit fe81eb53fa
Signed by: Bauke
GPG Key ID: C1C0F29952BCF558
11 changed files with 145 additions and 123 deletions

View File

@ -1,6 +1,6 @@
import browser from 'webextension-polyfill'; import browser from 'webextension-polyfill';
import {parseRedirect} from '../redirect/exports.js'; import storage from '../redirect/storage.js';
async function browserActionClicked() { async function browserActionClicked() {
await browser.runtime.openOptionsPage(); await browser.runtime.openOptionsPage();
@ -35,11 +35,8 @@ browser.webNavigation.onBeforeNavigate.addListener(async (details) => {
return; return;
} }
for (const [id, parameters] of Object.entries( for (const redirect of await storage.getRedirects()) {
await browser.storage.local.get(), if (!redirect.parameters.enabled) {
)) {
const redirect = parseRedirect(parameters, id);
if (redirect === undefined || !redirect.parameters.enabled) {
continue; continue;
} }

View File

@ -8,40 +8,26 @@ import {
narrowMatcherType, narrowMatcherType,
narrowRedirectType, narrowRedirectType,
parseRedirect, parseRedirect,
Redirects, Redirect,
RedirectParameters, RedirectParameters,
redirectTypes, redirectTypes,
} from '../../redirect/exports.js'; } from '../../redirect/exports.js';
import storage from '../../redirect/storage.js';
type Props = { type Props = {
id: string; redirect: Redirect;
redirect?: Redirects; removeRedirect: (id: number) => void;
removeRedirect: (id: string) => void; saveRedirect: (redirect: Redirect) => void;
saveRedirect: (redirect: Redirects) => void;
}; };
type State = { type State = RedirectParameters;
id: string;
redirectValue: string;
} & RedirectParameters;
export default class Editor extends Component<Props, State> { export default class Editor extends Component<Props, State> {
defaultParameters: RedirectParameters;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.defaultParameters = {
enabled: true,
matcherType: 'hostname',
matcherValue: '',
redirectType: 'simple',
redirectValue: '',
};
this.state = { this.state = {
id: this.props.id, ...props.redirect.parameters,
...this.parametersFromProps(),
}; };
} }
@ -87,21 +73,8 @@ export default class Editor extends Component<Props, State> {
this.onSelectChange(event, 'redirect'); this.onSelectChange(event, 'redirect');
}; };
parametersFromProps = (): RedirectParameters => { parseRedirect = (): Redirect => {
const redirect = this.props.redirect; const redirect = parseRedirect(this.state);
const parameters = redirect?.parameters ?? {...this.defaultParameters};
return {
enabled: parameters.enabled,
matcherType: parameters.matcherType,
matcherValue: parameters.matcherValue,
redirectType: parameters.redirectType,
redirectValue: parameters.redirectValue,
};
};
parseRedirect = (): Redirects => {
const redirect = parseRedirect(this.state, this.props.id);
if (redirect === undefined) { if (redirect === undefined) {
throw new Error('Failed to parse redirect'); throw new Error('Failed to parse redirect');
} }
@ -109,32 +82,24 @@ export default class Editor extends Component<Props, State> {
return redirect; return redirect;
}; };
prepareForStorage = (
parameters: RedirectParameters,
): Record<string, RedirectParameters> => {
const storage: Record<string, RedirectParameters> = {};
storage[this.props.id] = parameters;
return storage;
};
remove = async () => { remove = async () => {
await browser.storage.local.remove(this.props.id); const redirect = this.props.redirect;
this.props.removeRedirect(this.props.id); await browser.storage.local.remove(redirect.idString());
this.props.removeRedirect(redirect.parameters.id);
}; };
save = async () => { save = async () => {
const redirect = this.parseRedirect(); const redirect = this.parseRedirect();
await browser.storage.local.set( await storage.save(redirect);
this.prepareForStorage(redirect.parameters),
);
this.props.saveRedirect(redirect); this.props.saveRedirect(redirect);
}; };
toggleEnabled = async () => { toggleEnabled = async () => {
const enabled = !this.state.enabled; const enabled = !this.state.enabled;
const storage = this.prepareForStorage(this.parametersFromProps()); const redirect = this.props.redirect;
storage[this.props.id].enabled = enabled; const prepared = await storage.prepareForStorage(redirect);
await browser.storage.local.set(storage); prepared[redirect.idString()].enabled = enabled;
await browser.storage.local.set(prepared);
this.setState({enabled}); this.setState({enabled});
}; };

View File

@ -1,13 +1,8 @@
import {html} from 'htm/preact'; import {html} from 'htm/preact';
import {Component} from 'preact'; import {Component} from 'preact';
import browser from 'webextension-polyfill';
import { import {Redirect, SimpleRedirect} from '../../redirect/exports.js';
parseRedirect, import storage from '../../redirect/storage.js';
Redirect,
Redirects,
SimpleRedirect,
} from '../../redirect/exports.js';
import Editor from './editor.js'; import Editor from './editor.js';
import Usage from './usage.js'; import Usage from './usage.js';
@ -15,7 +10,7 @@ import Usage from './usage.js';
type Props = Record<string, unknown>; type Props = Record<string, unknown>;
type State = { type State = {
redirects: Redirects[]; redirects: Redirect[];
}; };
export class PageMain extends Component<Props, State> { export class PageMain extends Component<Props, State> {
@ -28,17 +23,7 @@ export class PageMain extends Component<Props, State> {
} }
async componentDidMount() { async componentDidMount() {
const redirects: Redirects[] = []; const redirects = await storage.getRedirects();
for (const [id, parameters] of Object.entries(
await browser.storage.local.get(),
)) {
const redirect = parseRedirect(parameters, id);
if (redirect === undefined) {
continue;
}
redirects.push(redirect);
}
// Sort the redirects by: // Sort the redirects by:
// * Matcher Type // * Matcher Type
@ -73,33 +58,36 @@ export class PageMain extends Component<Props, State> {
return rValueA.localeCompare(rValueB); return rValueA.localeCompare(rValueB);
}); });
this.setState({redirects}); this.setState({redirects});
} }
addNewRedirect = () => { addNewRedirect = async () => {
const redirect = new SimpleRedirect({
enabled: true,
id: await storage.nextRedirectId(),
matcherType: 'hostname',
matcherValue: 'example.com',
redirectType: 'simple',
redirectValue: 'example.org',
});
await storage.savePrepared(await storage.prepareForStorage(redirect));
this.setState({ this.setState({
redirects: [ redirects: [redirect, ...this.state.redirects],
new SimpleRedirect({
enabled: true,
matcherType: 'hostname',
matcherValue: 'example.com',
redirectType: 'simple',
redirectValue: 'example.org',
}),
...this.state.redirects,
],
}); });
}; };
removeRedirect = (id: string) => { removeRedirect = (id: number) => {
this.setState({ this.setState({
redirects: this.state.redirects.filter((redirect) => redirect.id !== id), redirects: this.state.redirects.filter(
(redirect) => redirect.parameters.id !== id,
),
}); });
}; };
saveRedirect = (redirect: Redirects) => { saveRedirect = (redirect: Redirect) => {
const redirectIndex = this.state.redirects.findIndex( const redirectIndex = this.state.redirects.findIndex(
(found) => found.id === redirect.id, (found) => found.parameters.id === redirect.parameters.id,
); );
if (redirectIndex === -1) { if (redirectIndex === -1) {
this.setState({ this.setState({
@ -117,8 +105,7 @@ export class PageMain extends Component<Props, State> {
(redirect) => (redirect) =>
html` html`
<${Editor} <${Editor}
key=${redirect.id} key=${redirect.idString()}
id=${redirect.id}
redirect=${redirect} redirect=${redirect}
removeRedirect=${this.removeRedirect} removeRedirect=${this.removeRedirect}
saveRedirect=${this.saveRedirect} saveRedirect=${this.saveRedirect}
@ -126,10 +113,6 @@ export class PageMain extends Component<Props, State> {
`, `,
); );
if (editors.length === 0) {
this.addNewRedirect();
}
return html` return html`
<main class="page-main"> <main class="page-main">
<div class="editors"> <div class="editors">

View File

@ -1,8 +1,12 @@
import {Redirect, RedirectParameters} from '../redirect/base.js'; import browser from 'webextension-polyfill';
import {RedirectParameters} from '../redirect/base.js';
import storage from '../redirect/storage.js';
const examples: RedirectParameters[] = [ const examples: RedirectParameters[] = [
{ {
enabled: true, enabled: true,
id: -1,
matcherType: 'hostname', matcherType: 'hostname',
matcherValue: 'twitter.com', matcherValue: 'twitter.com',
redirectType: 'hostname', redirectType: 'hostname',
@ -10,6 +14,7 @@ const examples: RedirectParameters[] = [
}, },
{ {
enabled: true, enabled: true,
id: -1,
matcherType: 'hostname', matcherType: 'hostname',
matcherValue: 'reddit.com', matcherValue: 'reddit.com',
redirectType: 'hostname', redirectType: 'hostname',
@ -17,6 +22,7 @@ const examples: RedirectParameters[] = [
}, },
{ {
enabled: true, enabled: true,
id: -1,
matcherType: 'regex', matcherType: 'regex',
matcherValue: '^https?://holllo\\.org/renav/?$', matcherValue: '^https?://holllo\\.org/renav/?$',
redirectType: 'simple', redirectType: 'simple',
@ -24,12 +30,17 @@ const examples: RedirectParameters[] = [
}, },
]; ];
export function generateExamples(): Record<string, RedirectParameters> { export async function generateExamples(): Promise<
const storage: Record<string, RedirectParameters> = {}; Record<string, RedirectParameters>
> {
const prepared: Record<string, RedirectParameters> = {};
let nextId = await storage.nextRedirectId();
for (const example of examples) { for (const example of examples) {
const id = Redirect.generateId(); example.id = nextId;
storage[id] = example; prepared[`redirect:${nextId}`] = example;
nextId += 1;
} }
return storage; await browser.storage.local.set({latestId: nextId - 1});
return prepared;
} }

View File

@ -1,7 +1,7 @@
import {html} from 'htm/preact'; import {html} from 'htm/preact';
import {Component, render} from 'preact'; import {Component, render} from 'preact';
import browser from 'webextension-polyfill';
import storage from '../redirect/storage.js';
import {PageFooter} from './components/page-footer.js'; import {PageFooter} from './components/page-footer.js';
import {PageHeader} from './components/page-header.js'; import {PageHeader} from './components/page-header.js';
import {PageMain} from './components/page-main.js'; import {PageMain} from './components/page-main.js';
@ -10,7 +10,7 @@ import {generateExamples} from './examples.js';
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
window.Holllo = { window.Holllo = {
async insertExamples() { async insertExamples() {
await browser.storage.local.set(generateExamples()); await storage.savePrepared(await generateExamples());
location.reload(); location.reload();
}, },
}; };

View File

@ -1,5 +1,3 @@
import {customAlphabet} from 'nanoid';
export const matcherTypes = ['hostname', 'regex'] as const; export const matcherTypes = ['hostname', 'regex'] as const;
export const redirectTypes = ['hostname', 'simple'] as const; export const redirectTypes = ['hostname', 'simple'] as const;
@ -16,6 +14,7 @@ export function narrowRedirectType(value: string): value is RedirectType {
export type RedirectParameters = { export type RedirectParameters = {
enabled: boolean; enabled: boolean;
id: number;
matcherType: MatcherType; matcherType: MatcherType;
matcherValue: string; matcherValue: string;
redirectType: RedirectType; redirectType: RedirectType;
@ -23,16 +22,14 @@ export type RedirectParameters = {
}; };
export abstract class Redirect { export abstract class Redirect {
public static generateId(): string { public static idString(id: number): string {
const alphabet = 'abcdefghijklmnopqrstuvwxyz'; return `redirect:${id}`;
const nanoid = customAlphabet(`${alphabet}${alphabet.toUpperCase()}`, 20);
return nanoid();
} }
public id: string; constructor(public parameters: RedirectParameters) {}
constructor(public parameters: RedirectParameters, id?: string) { public idString(): string {
this.id = id ?? Redirect.generateId(); return Redirect.idString(this.parameters.id);
} }
public isMatch(url: URL): boolean { public isMatch(url: URL): boolean {

View File

@ -10,15 +10,14 @@ export type Redirects = HostnameRedirect | SimpleRedirect;
export function parseRedirect( export function parseRedirect(
parameters: RedirectParameters, parameters: RedirectParameters,
id: string,
): Redirects | undefined { ): Redirects | undefined {
const redirectType = parameters?.redirectType; const redirectType = parameters?.redirectType;
if (redirectType === 'hostname') { if (redirectType === 'hostname') {
return new HostnameRedirect(parameters, id); return new HostnameRedirect(parameters);
} }
if (redirectType === 'simple') { if (redirectType === 'simple') {
return new SimpleRedirect(parameters, id); return new SimpleRedirect(parameters);
} }
} }

View File

@ -0,0 +1,68 @@
import browser from 'webextension-polyfill';
import {Redirect, RedirectParameters} from './base.js';
import {parseRedirect} from './exports.js';
const redirectKeyRegex = /^redirect:\d+$/i;
async function getRedirects(): Promise<Redirect[]> {
const redirects: Redirect[] = [];
const stored = await browser.storage.local.get();
for (const [key, value] of Object.entries(stored)) {
if (!redirectKeyRegex.test(key)) {
continue;
}
const redirect = parseRedirect(value);
if (redirect !== undefined) {
redirects.push(redirect);
}
}
return redirects;
}
async function nextRedirectId(): Promise<number> {
const {latestId} = await browser.storage.local.get('latestId');
const id = Number(latestId);
let nextId: number | undefined;
if (Number.isNaN(id)) {
const redirects = await getRedirects();
nextId = redirects.length + 1;
} else {
nextId = id + 1;
}
await browser.storage.local.set({latestId: nextId});
return nextId;
}
async function prepareForStorage(
redirect: Redirect,
): Promise<Record<string, RedirectParameters>> {
const prepared: Record<string, RedirectParameters> = {};
prepared[redirect.idString()] = redirect.parameters;
return prepared;
}
async function save(redirect: Redirect): Promise<void> {
await savePrepared(await prepareForStorage(redirect));
}
async function savePrepared(
prepared: Record<string, RedirectParameters>,
): Promise<void> {
await browser.storage.local.set(prepared);
}
const storage = {
getRedirects,
nextRedirectId,
prepareForStorage,
redirectKeyRegex,
save,
savePrepared,
};
export default storage;

View File

@ -17,6 +17,7 @@ import {
const hostnameParameters: RedirectParameters = { const hostnameParameters: RedirectParameters = {
enabled: true, enabled: true,
id: 1,
matcherType: 'hostname', matcherType: 'hostname',
matcherValue: 'example.com', matcherValue: 'example.com',
redirectType: 'hostname', redirectType: 'hostname',
@ -25,6 +26,7 @@ const hostnameParameters: RedirectParameters = {
const simpleParameters: RedirectParameters = { const simpleParameters: RedirectParameters = {
enabled: true, enabled: true,
id: 2,
matcherType: 'hostname', matcherType: 'hostname',
matcherValue: 'example.com', matcherValue: 'example.com',
redirectType: 'simple', redirectType: 'simple',
@ -42,13 +44,12 @@ test('parseRedirect', (t) => {
]; ];
for (const sample of samples) { for (const sample of samples) {
const redirect = parseRedirect(sample, Redirect.generateId()); const redirect = parseRedirect(sample);
if (redirect === undefined) { if (redirect === undefined) {
t.pass('parseRedirect returned undefined'); t.pass('parseRedirect returned undefined');
} else { } else {
t.regex(redirect.id, /^[a-z]{20}$/i); t.regex(redirect?.idString(), /^redirect:\d+$/i);
redirect.id = 'id';
t.snapshot(redirect, `Class ${redirect.constructor.name}`); t.snapshot(redirect, `Class ${redirect.constructor.name}`);
} }
} }
@ -92,6 +93,7 @@ test('Redirect.isMatch', (t) => {
const regexMatch = new HostnameRedirect({ const regexMatch = new HostnameRedirect({
enabled: true, enabled: true,
id: 3,
matcherType: 'regex', matcherType: 'regex',
matcherValue: String.raw`^https://(www\.)?example.org/$`, matcherValue: String.raw`^https://(www\.)?example.org/$`,
redirectType: 'simple', redirectType: 'simple',

View File

@ -9,9 +9,9 @@ Generated by [AVA](https://avajs.dev).
> Class HostnameRedirect > Class HostnameRedirect
HostnameRedirect { HostnameRedirect {
id: 'id',
parameters: { parameters: {
enabled: true, enabled: true,
id: 1,
matcherType: 'hostname', matcherType: 'hostname',
matcherValue: 'example.com', matcherValue: 'example.com',
redirectType: 'hostname', redirectType: 'hostname',
@ -22,9 +22,9 @@ Generated by [AVA](https://avajs.dev).
> Class SimpleRedirect > Class SimpleRedirect
SimpleRedirect { SimpleRedirect {
id: 'id',
parameters: { parameters: {
enabled: true, enabled: true,
id: 2,
matcherType: 'hostname', matcherType: 'hostname',
matcherValue: 'example.com', matcherValue: 'example.com',
redirectType: 'simple', redirectType: 'simple',