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

View File

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

View File

@ -1,13 +1,8 @@
import {html} from 'htm/preact';
import {Component} from 'preact';
import browser from 'webextension-polyfill';
import {
parseRedirect,
Redirect,
Redirects,
SimpleRedirect,
} from '../../redirect/exports.js';
import {Redirect, SimpleRedirect} from '../../redirect/exports.js';
import storage from '../../redirect/storage.js';
import Editor from './editor.js';
import Usage from './usage.js';
@ -15,7 +10,7 @@ import Usage from './usage.js';
type Props = Record<string, unknown>;
type State = {
redirects: Redirects[];
redirects: Redirect[];
};
export class PageMain extends Component<Props, State> {
@ -28,17 +23,7 @@ export class PageMain extends Component<Props, State> {
}
async componentDidMount() {
const redirects: Redirects[] = [];
for (const [id, parameters] of Object.entries(
await browser.storage.local.get(),
)) {
const redirect = parseRedirect(parameters, id);
if (redirect === undefined) {
continue;
}
redirects.push(redirect);
}
const redirects = await storage.getRedirects();
// Sort the redirects by:
// * Matcher Type
@ -73,33 +58,36 @@ export class PageMain extends Component<Props, State> {
return rValueA.localeCompare(rValueB);
});
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({
redirects: [
new SimpleRedirect({
enabled: true,
matcherType: 'hostname',
matcherValue: 'example.com',
redirectType: 'simple',
redirectValue: 'example.org',
}),
...this.state.redirects,
],
redirects: [redirect, ...this.state.redirects],
});
};
removeRedirect = (id: string) => {
removeRedirect = (id: number) => {
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(
(found) => found.id === redirect.id,
(found) => found.parameters.id === redirect.parameters.id,
);
if (redirectIndex === -1) {
this.setState({
@ -117,8 +105,7 @@ export class PageMain extends Component<Props, State> {
(redirect) =>
html`
<${Editor}
key=${redirect.id}
id=${redirect.id}
key=${redirect.idString()}
redirect=${redirect}
removeRedirect=${this.removeRedirect}
saveRedirect=${this.saveRedirect}
@ -126,10 +113,6 @@ export class PageMain extends Component<Props, State> {
`,
);
if (editors.length === 0) {
this.addNewRedirect();
}
return html`
<main class="page-main">
<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[] = [
{
enabled: true,
id: -1,
matcherType: 'hostname',
matcherValue: 'twitter.com',
redirectType: 'hostname',
@ -10,6 +14,7 @@ const examples: RedirectParameters[] = [
},
{
enabled: true,
id: -1,
matcherType: 'hostname',
matcherValue: 'reddit.com',
redirectType: 'hostname',
@ -17,6 +22,7 @@ const examples: RedirectParameters[] = [
},
{
enabled: true,
id: -1,
matcherType: 'regex',
matcherValue: '^https?://holllo\\.org/renav/?$',
redirectType: 'simple',
@ -24,12 +30,17 @@ const examples: RedirectParameters[] = [
},
];
export function generateExamples(): Record<string, RedirectParameters> {
const storage: Record<string, RedirectParameters> = {};
export async function generateExamples(): Promise<
Record<string, RedirectParameters>
> {
const prepared: Record<string, RedirectParameters> = {};
let nextId = await storage.nextRedirectId();
for (const example of examples) {
const id = Redirect.generateId();
storage[id] = example;
example.id = nextId;
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 {Component, render} from 'preact';
import browser from 'webextension-polyfill';
import storage from '../redirect/storage.js';
import {PageFooter} from './components/page-footer.js';
import {PageHeader} from './components/page-header.js';
import {PageMain} from './components/page-main.js';
@ -10,7 +10,7 @@ import {generateExamples} from './examples.js';
window.addEventListener('DOMContentLoaded', () => {
window.Holllo = {
async insertExamples() {
await browser.storage.local.set(generateExamples());
await storage.savePrepared(await generateExamples());
location.reload();
},
};

View File

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

View File

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

View File

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