spica
Version:
Supervisor, Coroutine, Channel, select, AtomicPromise, Cancellation, Cache, List, Queue, Stack, and some utils.
209 lines (194 loc) • 6.28 kB
text/typescript
import '../global';
import { Mutable } from '../type';
import { memoize } from '../memoize';
import { TLRU } from '../tlru';
declare class Absolute {
private static readonly IDENTITY: unique symbol;
private readonly [Absolute.IDENTITY];
}
declare class Encoded {
private static readonly IDENTITY: unique symbol;
private readonly [Encoded.IDENTITY];
}
declare class Identity<T> {
private static readonly IDENTITY: unique symbol;
private readonly [Identity.IDENTITY]: T;
}
type URL<T> = Identity<T> & string;
// https://www.ietf.org/rfc/rfc3986.txt
export type StandardURL = URL<Encoded & Absolute>;
export type AbsoluteURL = URL<Absolute>;
export function standardize(url: URL<unknown>, base?: string): void;
export function standardize(url: string, base?: string): StandardURL;
export function standardize(url: string, base?: string): StandardURL {
const { origin, protocol, href } = new ReadonlyURL(url, base!);
url = origin === 'null'
? protocol.toLowerCase() + href.slice(protocol.length)
: origin.toLowerCase() + href.slice(origin.length);
return encode(url as AbsoluteURL);
}
type EncodedURL<T = Encoded> = URL<Encoded & T>;
export function encode(url: EncodedURL): void;
export function encode<T>(url: URL<T>): EncodedURL<T>;
export function encode(url: string): EncodedURL;
export function encode(url: string): EncodedURL {
assert(url === url.trim());
url = url.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, '');
const { 1: base, 2: hash } = url
.match(/^([^#]*)(.*)$/s)!;
const { 1: path, 2: query } = base
.replace(/(?:%(?:[0-9][a-f]|[a-f][0-9a-fA-F]|[A-F][0-9a-f]))+/g, str => str.toUpperCase())
.match(/^([^?]*)(.*)$/s)!;
return ''
+ path.replace(/(?:[^%[\]]|%(?![0-9A-F]{2}))+/ig, encodeURI)
+ query.replace(/(?!^)(?:[^%=&]|%(?![0-9A-F]{2}))+/ig, encodeURIComponent)
+ hash as EncodedURL;
}
type CachedURL<T extends string> = Partial<Mutable<global.URL>> & {
url: global.URL;
href?: T;
resource?: string;
scheme?: string;
path?: string;
query?: string;
fragment?: string;
};
export class ReadonlyURL<T extends string = string> implements Readonly<global.URL> {
// Can't freeze URL object in the Firefox extension environment.
// ref: https://github.com/falsandtru/pjax-api/issues/44#issuecomment-633915035
// Bug: Error in dependents.
// @ts-ignore
private static readonly get = memoize((url: string, base: string | undefined): CachedURL =>
({ url: new global.URL(url, base) }),
(url, base = '') => `${base.indexOf('\n') > -1 ? base.replace(/\n+/g, '') : base}\n${url}`,
new TLRU(10000));
constructor(url: T, ...base:
T extends AbsoluteURL | `${string}:${string}` ? [string?] :
T extends `${infer _}` ? [string] :
[T]);
constructor(source: string, base?: string) {
source = source.trim();
base = base?.trim();
switch (source.slice(0, source.lastIndexOf('://', 9) + 1).toLowerCase()) {
case 'http:':
case 'https:':
base = undefined;
break;
default:
switch (base?.slice(0, base.lastIndexOf('://', 9) + 1).toLowerCase()) {
case 'http:':
case 'https:':
const i = base.indexOf('#');
if (i > -1) {
base = base.slice(0, i);
}
const j = base.indexOf('?');
if (j > -1 && source !== '' && source[0] !== '#') {
base = base.slice(0, j);
}
}
}
this.cache = ReadonlyURL.get(source, base);
this.params = undefined;
this.source = source;
this.base = base;
}
private readonly cache: CachedURL<T>;
private params?: ReadonlyURLSearchParams;
public readonly source: string;
public readonly base?: string;
public get href(): T {
return this.cache.href
??= this.cache.url.href as T;
}
public get resource(): string {
return this.cache.resource
??= this.href.slice(0, this.href.search(/[?#]|$/)) + this.search;
}
public get origin(): string {
return this.cache.origin
??= this.cache.url.origin;
}
public get scheme(): string {
return this.cache.scheme
??= this.protocol.slice(0, -1);
}
public get protocol(): string {
return this.cache.protocol
??= this.cache.url.protocol;
}
public get username(): string {
return this.cache.username
??= this.cache.url.username;
}
public get password(): string {
return this.cache.password
??= this.cache.url.password;
}
public get host(): string {
return this.cache.host
??= this.cache.url.host;
}
public get hostname(): string {
return this.cache.hostname
??= this.cache.url.hostname;
}
public get port(): string {
return this.cache.port
??= this.cache.url.port;
}
public get path(): string {
return this.cache.path
??= `${this.pathname}${this.search}`;
}
public get pathname(): string {
return this.cache.pathname
??= this.cache.url.pathname;
}
public get search(): string {
return this.cache.search
??= this.cache.url.search;
}
public get query(): string {
return this.cache.query
??= this.search || this.href[this.href.length - this.fragment.length - 1] === '?' && '?' || '';
}
public get hash(): string {
return this.cache.hash
??= this.cache.url.hash;
}
public get fragment(): string {
return this.cache.fragment
??= this.hash || this.href[this.href.length - 1] === '#' && '#' || '';
}
public get searchParams(): ReadonlyURLSearchParams {
return this.params
??= new ReadonlyURLSearchParams(this.search);
}
public toString(): string {
return this.href;
}
public toJSON(): string {
return this.href;
}
}
class ReadonlyURLSearchParams extends URLSearchParams {
public override append(name: string, value: string): never {
this.sort();
name;
value;
}
public override delete(name: string, value?: string): never {
this.sort();
name;
value;
}
public override set(name: string, value: string): never {
this.sort();
name;
value;
}
public override sort(): never {
throw new Error('Spica: URL: Cannot use mutable methods with ReadonlyURLSearchParams');
}
}