utilium 2.0.0-pre.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cache.ts DELETED
@@ -1,241 +0,0 @@
1
- /** A ranged cache */
2
- import { extendBuffer } from './buffer.js';
3
-
4
- export interface Options {
5
- /**
6
- * If true, use multiple buffers to cache a file.
7
- * This is useful when working with small parts of large files,
8
- * since we don't need to allocate a large buffer that is mostly unused
9
- * @default true
10
- */
11
- sparse?: boolean;
12
-
13
- /**
14
- * The threshold for whether to combine regions or not
15
- * @see Region
16
- * @default 0xfff // 4 KiB
17
- */
18
- regionGapThreshold?: number;
19
-
20
- /**
21
- * Whether to only update the cache when changing or deleting resources
22
- * @default false
23
- */
24
- cacheOnly?: boolean;
25
- }
26
-
27
- export type Range = { start: number; end: number };
28
-
29
- export interface Region {
30
- /** The region's offset from the start of the resource */
31
- offset: number;
32
-
33
- /** Ranges cached in this region. These are absolute! */
34
- ranges: Range[];
35
-
36
- /** Data for this region */
37
- data: Uint8Array;
38
- }
39
-
40
- /**
41
- * The cache for a specific resource
42
- * @internal
43
- */
44
- export class Resource<ID> {
45
- /** Regions used to reduce unneeded allocations. Think of sparse arrays. */
46
- public readonly regions: Region[] = [];
47
-
48
- /** The full size of the resource */
49
- public get size() {
50
- return this._size;
51
- }
52
-
53
- public set size(value: number) {
54
- if (value >= this._size) {
55
- this._size = value;
56
- return;
57
- }
58
-
59
- this._size = value;
60
-
61
- for (let i = this.regions.length - 1; i >= 0; i--) {
62
- const region = this.regions[i];
63
-
64
- if (region.offset >= value) {
65
- this.regions.splice(i, 1);
66
- continue;
67
- }
68
-
69
- const maxLength = value - region.offset;
70
- if (region.data.byteLength > maxLength) {
71
- region.data = region.data.subarray(0, maxLength);
72
- }
73
-
74
- region.ranges = region.ranges
75
- .filter(range => range.start < value)
76
- .map(range => {
77
- if (range.end > value) {
78
- return { start: range.start, end: value };
79
- }
80
- return range;
81
- });
82
- }
83
- }
84
-
85
- public constructor(
86
- /** The resource ID */
87
- public readonly id: ID,
88
- protected _size: number,
89
- protected readonly options: Options,
90
- resources?: Map<ID, Resource<ID> | undefined>
91
- ) {
92
- options.sparse ??= true;
93
- if (!options.sparse) this.regions.push({ offset: 0, data: new Uint8Array(_size), ranges: [] });
94
-
95
- resources?.set(id, this);
96
- }
97
-
98
- /** Combines adjacent regions and combines adjacent ranges within a region */
99
- public collect(): void {
100
- if (!this.options.sparse) return;
101
- const { regionGapThreshold = 0xfff } = this.options;
102
-
103
- for (let i = 0; i < this.regions.length - 1; ) {
104
- const current = this.regions[i];
105
- const next = this.regions[i + 1];
106
-
107
- if (next.offset - (current.offset + current.data.byteLength) > regionGapThreshold) {
108
- i++;
109
- continue;
110
- }
111
-
112
- // Combine ranges
113
- current.ranges.push(...next.ranges);
114
- current.ranges.sort((a, b) => a.start - b.start);
115
-
116
- // Combine overlapping/adjacent ranges
117
- current.ranges = current.ranges.reduce((acc: Range[], range) => {
118
- if (!acc.length || acc.at(-1)!.end < range.start) {
119
- acc.push(range);
120
- } else {
121
- acc.at(-1)!.end = Math.max(acc.at(-1)!.end, range.end);
122
- }
123
- return acc;
124
- }, []);
125
-
126
- // Extend buffer to include the new region
127
- current.data = extendBuffer(current.data, next.offset + next.data.byteLength);
128
- current.data.set(next.data, next.offset - current.offset);
129
-
130
- // Remove the next region after merging
131
- this.regions.splice(i + 1, 1);
132
- }
133
- }
134
-
135
- /** Takes an initial range and finds the sub-ranges that are not in the cache */
136
- public missing(start: number, end: number): Range[] {
137
- const missingRanges: Range[] = [];
138
-
139
- for (const region of this.regions) {
140
- if (region.offset >= end) break;
141
-
142
- for (const range of region.ranges) {
143
- if (range.end <= start) continue;
144
- if (range.start >= end) break;
145
-
146
- if (range.start > start) {
147
- missingRanges.push({ start, end: Math.min(range.start, end) });
148
- }
149
-
150
- // Adjust the current start if the region overlaps
151
- if (range.end > start) start = Math.max(start, range.end);
152
-
153
- if (start >= end) break;
154
- }
155
-
156
- if (start >= end) break;
157
- }
158
-
159
- // If there are still missing parts at the end
160
- if (start < end) missingRanges.push({ start, end });
161
-
162
- return missingRanges;
163
- }
164
-
165
- /**
166
- * Get the cached sub-ranges of an initial range.
167
- * This is conceptually the inverse of `missing`.
168
- */
169
- public cached(start: number, end: number): Range[] {
170
- const cachedRanges: Range[] = [];
171
-
172
- for (const region of this.regions) {
173
- if (region.offset >= end) break;
174
-
175
- for (const range of region.ranges) {
176
- if (range.end <= start) continue;
177
- if (range.start >= end) break;
178
-
179
- cachedRanges.push({
180
- start: Math.max(start, range.start),
181
- end: Math.min(end, range.end),
182
- });
183
- }
184
- }
185
-
186
- cachedRanges.sort((a, b) => a.start - b.start);
187
- const merged: Range[] = [];
188
- for (const curr of cachedRanges) {
189
- const last = merged.at(-1);
190
- if (last && curr.start <= last.end) {
191
- last.end = Math.max(last.end, curr.end);
192
- } else {
193
- merged.push(curr);
194
- }
195
- }
196
-
197
- return merged;
198
- }
199
-
200
- /** Get the region who's ranges include an offset */
201
- public regionAt(offset: number): Region | undefined {
202
- if (!this.regions.length) return;
203
-
204
- for (const region of this.regions) {
205
- if (region.offset > offset) break;
206
-
207
- // Check if the offset is within this region
208
- if (offset >= region.offset && offset < region.offset + region.data.byteLength) return region;
209
- }
210
- }
211
-
212
- /** Add new data to the cache at given specified offset */
213
- public add(data: Uint8Array, offset: number): this {
214
- const end = offset + data.byteLength;
215
- const region = this.regionAt(offset);
216
-
217
- if (region) {
218
- region.data = extendBuffer(region.data, end);
219
- region.data.set(data, offset);
220
- region.ranges.push({ start: offset, end });
221
- region.ranges.sort((a, b) => a.start - b.start);
222
-
223
- this.collect();
224
- return this;
225
- }
226
-
227
- // Find the correct index to insert the new region
228
- const newRegion: Region = { data, offset: offset, ranges: [{ start: offset, end }] };
229
- const insertIndex = this.regions.findIndex(region => region.offset > offset);
230
-
231
- // Insert at the right index to keep regions sorted
232
- if (insertIndex == -1) {
233
- this.regions.push(newRegion); // Append if no later region exists
234
- } else {
235
- this.regions.splice(insertIndex, 0, newRegion); // Insert before the first region with a greater offset
236
- }
237
-
238
- this.collect();
239
- return this;
240
- }
241
- }
package/src/checksum.ts DELETED
@@ -1,22 +0,0 @@
1
- /** Utilities for computing checksums */
2
-
3
- // Precompute CRC32C table
4
- const crc32cTable = new Uint32Array(256);
5
- for (let i = 0; i < 256; i++) {
6
- let value = i;
7
- for (let j = 0; j < 8; j++) {
8
- value = value & 1 ? 0x82f63b78 ^ (value >>> 1) : value >>> 1;
9
- }
10
- crc32cTable[i] = value;
11
- }
12
-
13
- /**
14
- * Computes the CRC32C checksum of a Uint8Array.
15
- */
16
- export function crc32c(data: Uint8Array): number {
17
- let crc = 0xffffffff;
18
- for (let i = 0; i < data.length; i++) {
19
- crc = (crc >>> 8) ^ crc32cTable[(crc ^ data[i]) & 0xff];
20
- }
21
- return (crc ^ 0xffffffff) >>> 0;
22
- }
package/src/debugging.ts DELETED
@@ -1,113 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-base-to-string */
2
- export interface CreateLoggerOptions {
3
- /**
4
- * The function used to output
5
- * @default console.log
6
- */
7
- output?: (...args: any[]) => void;
8
- stringify?: (value: unknown) => string;
9
- /**
10
- * Whether to output the class name when logging methods
11
- * @default true
12
- */
13
- className?: boolean;
14
- /**
15
- * The separator used to separate the class name and method.
16
- * Ignored if `className` is `false`
17
- * @default '#'
18
- */
19
- separator?: string;
20
- /**
21
- * Whether to log the return value
22
- * @default false
23
- */
24
- returnValue?: boolean;
25
- }
26
-
27
- function defaultStringify(value: unknown): string {
28
- switch (typeof value) {
29
- case 'undefined':
30
- return 'undefined';
31
- case 'string':
32
- case 'number':
33
- case 'boolean':
34
- return JSON.stringify(value);
35
- case 'bigint':
36
- case 'symbol':
37
- return value.toString();
38
- case 'function':
39
- return value.name + '()';
40
- case 'object':
41
- if (value === null) return 'null';
42
- if (ArrayBuffer.isView(value)) {
43
- const length =
44
- 'length' in value
45
- ? (value.length as number)
46
- : value.byteLength / (value.constructor as any).BYTES_PER_ELEMENT;
47
- return `${value.constructor.name.replaceAll('Array', '').toLowerCase()}[${length}]`;
48
- }
49
- if (Array.isArray(value)) return `unknown[${value.length}]`;
50
- try {
51
- const json = JSON.stringify(value);
52
-
53
- return json.length < 100 ? json : value.toString();
54
- } catch {
55
- return value.toString();
56
- }
57
- }
58
- }
59
-
60
- type LoggableDecoratorContext = Exclude<DecoratorContext, ClassFieldDecoratorContext>;
61
-
62
- /**
63
- * Create a function that can be used to decorate classes and non-field members.
64
- */
65
- export function createLogDecorator(options: CreateLoggerOptions) {
66
- const {
67
- output = console.log,
68
- separator = '#',
69
- returnValue = false,
70
- stringify = defaultStringify,
71
- className = true,
72
- } = options;
73
-
74
- return function log<T extends (...args: any[]) => any>(value: T, context: LoggableDecoratorContext): T {
75
- if (context.kind == 'class') {
76
- return function (...args: any[]) {
77
- output(`new ${value.name} (${args.map(stringify).join(', ')})`);
78
- return Reflect.construct(value, args);
79
- } as T;
80
- }
81
-
82
- return function (this: any, ...args: any[]) {
83
- const prefix = (className ? this.constructor.name + separator : '') + context.name.toString();
84
-
85
- output(`${prefix}(${args.map(stringify).join(', ')})`);
86
-
87
- const result = value.call(this, ...args);
88
-
89
- if (returnValue) output(' => ' + stringify(result));
90
-
91
- return result;
92
- } as T;
93
- };
94
- }
95
-
96
- /**
97
- * @internal @hidden
98
- */
99
- export let U_DEBUG = 'process' in globalThis && 'env' in globalThis.process && globalThis.process.env.U_DEBUG == 'true';
100
-
101
- /**
102
- * @internal @hidden
103
- */
104
- export function _setDebug(value: boolean) {
105
- U_DEBUG = value;
106
- }
107
-
108
- /**
109
- * @internal @hidden
110
- */
111
- export function _debugLog(...text: any[]) {
112
- if (U_DEBUG) console.debug('[U]', ...text);
113
- }
package/src/dom.ts DELETED
@@ -1,43 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unused-expressions */
2
-
3
- /**
4
- * Upload a file
5
- * @todo use Promise.withResolvers
6
- */
7
- export function upload(type?: string, multiple = false): Promise<File> {
8
- return new Promise<File>((resolve, reject) => {
9
- const input = document.createElement('input');
10
- input.type = 'file';
11
- if (type) input.accept = type;
12
-
13
- if (multiple) input.multiple = true;
14
-
15
- input.addEventListener('change', e => {
16
- const file = input.files?.[0];
17
- file ? resolve(file) : reject(new ReferenceError('No files uploaded'));
18
- });
19
-
20
- input.click();
21
- });
22
- }
23
-
24
- /**
25
- * Downloads some data
26
- */
27
- export function download(data: BlobPart, name: string): void {
28
- const link = document.createElement('a');
29
- link.href = URL.createObjectURL(new Blob([data]));
30
- link.download = name;
31
- link.click();
32
- }
33
-
34
- /**
35
- * Create an instance of a `<template>`
36
- */
37
- export function cloneTemplate(selector: string): DocumentFragment {
38
- const template = document.querySelector<HTMLTemplateElement>(selector);
39
- if (!template) {
40
- throw new ReferenceError('Template does not exist: ' + selector);
41
- }
42
- return template.content.cloneNode(true) as DocumentFragment;
43
- }
package/src/fs.ts DELETED
@@ -1,213 +0,0 @@
1
- import { isJSON, type JSONValue } from './objects.js';
2
- import * as fs from 'fs';
3
-
4
- export abstract class FileMap<V> implements Map<string, V> {
5
- public get [Symbol.toStringTag](): string {
6
- return 'FileMap';
7
- }
8
-
9
- public constructor(protected readonly path: string) {
10
- if (!path) {
11
- throw new ReferenceError('No path specified');
12
- }
13
- }
14
-
15
- protected abstract readonly _map: Map<string, V>;
16
-
17
- public abstract clear(): void;
18
-
19
- public abstract delete(key: string): boolean;
20
-
21
- public abstract get(key: string): V;
22
-
23
- public abstract has(key: string): boolean;
24
-
25
- public abstract set(key: string, value: V): this;
26
-
27
- public get size() {
28
- return this._map.size;
29
- }
30
-
31
- public get [Symbol.iterator]() {
32
- return this._map[Symbol.iterator].bind(this._map);
33
- }
34
-
35
- public get keys(): typeof this._map.keys {
36
- return this._map.keys.bind(this._map);
37
- }
38
-
39
- public get values(): typeof this._map.values {
40
- return this._map.values.bind(this._map);
41
- }
42
-
43
- public get entries(): typeof this._map.entries {
44
- return this._map.entries.bind(this._map);
45
- }
46
-
47
- public get forEach(): typeof this._map.forEach {
48
- return this._map.forEach.bind(this._map);
49
- }
50
- }
51
-
52
- export interface JSONFileMapOptions {
53
- /**
54
- * Should an invalid JSON file be overwritten
55
- */
56
- overwrite_invalid: boolean;
57
- }
58
-
59
- /**
60
- * A Map overlaying a JSON file
61
- */
62
- export class JSONFileMap<T extends JSONValue = JSONValue> extends FileMap<T> {
63
- public get [Symbol.toStringTag](): 'JSONFileMap' {
64
- return 'JSONFileMap';
65
- }
66
-
67
- public constructor(
68
- path: string,
69
- public readonly options: JSONFileMapOptions
70
- ) {
71
- super(path);
72
- if (!fs.existsSync(path)) {
73
- fs.writeFileSync(path, '{}');
74
- }
75
- }
76
-
77
- public get _map(): Map<string, T> {
78
- const content = fs.readFileSync(this.path, 'utf8');
79
- if (!isJSON(content)) {
80
- if (!this.options.overwrite_invalid) {
81
- throw new SyntaxError('Invalid JSON file: ' + this.path);
82
- }
83
- console.warn('Invalid JSON file (overwriting): ' + this.path);
84
- this.clear();
85
- return new Map();
86
- }
87
- return new Map(Object.entries(JSON.parse(content)));
88
- }
89
-
90
- public _write(map: Map<string, T>) {
91
- if (!fs.existsSync(this.path)) {
92
- fs.writeFileSync(this.path, '{}');
93
- }
94
- const content = JSON.stringify(Object.fromEntries(map));
95
- fs.writeFileSync(this.path, content);
96
- }
97
-
98
- public clear() {
99
- fs.writeFileSync(this.path, '{}');
100
- }
101
-
102
- public delete(key: string): boolean {
103
- const map = this._map;
104
- const rt = map.delete(key);
105
- this._write(map);
106
- return rt;
107
- }
108
-
109
- public get<U extends T>(key: string): U {
110
- return this._map.get(key) as U;
111
- }
112
-
113
- public has(key: string): boolean {
114
- return this._map.has(key);
115
- }
116
-
117
- public set(key: string, value: T): this {
118
- const map = this._map;
119
- map.set(key, value);
120
- this._write(map);
121
- return this;
122
- }
123
- }
124
-
125
- export interface FolderMapOptions {
126
- /**
127
- * Suffix to append to keys to resolve file names
128
- */
129
- suffix: string;
130
- }
131
-
132
- /**
133
- * A Map overlaying a folder
134
- */
135
- export class FolderMap extends FileMap<string> {
136
- public get [Symbol.toStringTag](): 'FolderMap' {
137
- return 'FolderMap';
138
- }
139
-
140
- public constructor(
141
- path: string,
142
- public readonly options: Partial<FolderMapOptions>
143
- ) {
144
- super(path);
145
- }
146
-
147
- protected get _names(): string[] {
148
- return fs
149
- .readdirSync(this.path)
150
- .filter(p => p.endsWith(this.options.suffix || ''))
151
- .map(p => p.slice(0, -this.options.suffix!.length));
152
- }
153
-
154
- protected _join(path: string): string {
155
- return `${this.path}/${path}${this.options.suffix}`;
156
- }
157
-
158
- protected get _map(): Map<string, string> {
159
- const entries: [string, string][] = [];
160
- for (const name of this._names) {
161
- const content = fs.readFileSync(this._join(name), 'utf8');
162
- entries.push([name, content]);
163
- }
164
- return new Map(entries);
165
- }
166
-
167
- public clear(): void {
168
- for (const name of this._names) {
169
- fs.unlinkSync(this._join(name));
170
- }
171
- }
172
-
173
- public delete(key: string): boolean {
174
- if (!this.has(key)) {
175
- return false;
176
- }
177
-
178
- fs.unlinkSync(this._join(key));
179
- return true;
180
- }
181
-
182
- public get(key: string): string {
183
- if (!this.has(key)) {
184
- throw new ReferenceError('Key not found');
185
- }
186
- return fs.readFileSync(this._join(key), 'utf8');
187
- }
188
-
189
- public has(key: string): boolean {
190
- return this._names.includes(key);
191
- }
192
-
193
- public set(key: string, value: string): this {
194
- fs.writeFileSync(this._join(key), value);
195
- return this;
196
- }
197
- }
198
-
199
- export function gitCommitHash(repo: string = '.'): string {
200
- repo = repo.replaceAll(/\/+/g, '/').replaceAll(/\/$/g, '');
201
- const rev = fs
202
- .readFileSync(repo + '/.git/HEAD')
203
- .toString()
204
- .trim();
205
- if (rev.indexOf(':') === -1) {
206
- return rev;
207
- } else {
208
- return fs
209
- .readFileSync(repo + '/.git/' + rev.substring(5))
210
- .toString()
211
- .trim();
212
- }
213
- }
package/src/index.ts DELETED
@@ -1,12 +0,0 @@
1
- // This file should not be added to
2
- // For better tree shaking, import from whichever file is actually needed
3
-
4
- export * from './list.js';
5
- export * from './misc.js';
6
- export * from './numbers.js';
7
- export * from './objects.js';
8
- export * from './random.js';
9
- export * from './string.js';
10
- export * from './struct.js';
11
- export * from './types.js';
12
- export * as version from './version.js';