Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d29d0d5121 | |||
| 5b3bbdf02c | |||
| 6b379270a9 | |||
| 28716c7b5a | |||
| d530f6abdf |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ uploads
|
|||||||
public/momentum*js
|
public/momentum*js
|
||||||
junit.xml
|
junit.xml
|
||||||
/docs/
|
/docs/
|
||||||
|
main.mjs
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
6927
package-lock.json
generated
Normal file
6927
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ztimson/utils",
|
"name": "@ztimson/utils",
|
||||||
"version": "0.29.2",
|
"version": "0.29.7",
|
||||||
"description": "Utility library",
|
"description": "Utility library",
|
||||||
"author": "Zak Timson",
|
"author": "Zak Timson",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
24
src/files.ts
24
src/files.ts
@@ -83,6 +83,7 @@ export function timestampFilename(name?: string, date: Date | number | string =
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload file to URL with progress callback using PromiseProgress
|
* Upload file to URL with progress callback using PromiseProgress
|
||||||
|
* Works in both browser (with progress) and Node.js (fallback without progress)
|
||||||
*
|
*
|
||||||
* @param {{url: string, files: File[], headers?: {[p: string]: string}, withCredentials?: boolean}} options
|
* @param {{url: string, files: File[], headers?: {[p: string]: string}, withCredentials?: boolean}} options
|
||||||
* @return {PromiseProgress<T>} Promise of request with `onProgress` callback
|
* @return {PromiseProgress<T>} Promise of request with `onProgress` callback
|
||||||
@@ -93,10 +94,12 @@ export function uploadWithProgress<T>(options: {
|
|||||||
headers?: {[key: string]: string};
|
headers?: {[key: string]: string};
|
||||||
withCredentials?: boolean;
|
withCredentials?: boolean;
|
||||||
}): PromiseProgress<T> {
|
}): PromiseProgress<T> {
|
||||||
|
// Browser environment - use XMLHttpRequest for progress
|
||||||
|
if (typeof XMLHttpRequest !== 'undefined') {
|
||||||
return new PromiseProgress<T>((res, rej, prog) => {
|
return new PromiseProgress<T>((res, rej, prog) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
options.files.forEach(f => formData.append('file', f));
|
options.files.forEach(f => formData.append('files', f));
|
||||||
|
|
||||||
xhr.withCredentials = !!options.withCredentials;
|
xhr.withCredentials = !!options.withCredentials;
|
||||||
xhr.upload.addEventListener('progress', (event) => event.lengthComputable ? prog(event.loaded / event.total) : null);
|
xhr.upload.addEventListener('progress', (event) => event.lengthComputable ? prog(event.loaded / event.total) : null);
|
||||||
@@ -108,4 +111,23 @@ export function uploadWithProgress<T>(options: {
|
|||||||
Object.entries(options.headers || {}).forEach(([key, value]) => xhr.setRequestHeader(key, value));
|
Object.entries(options.headers || {}).forEach(([key, value]) => xhr.setRequestHeader(key, value));
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node.js environment - fallback to fetch without progress
|
||||||
|
return new PromiseProgress<T>(async (res, rej) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
options.files.forEach(f => formData.append('files', f));
|
||||||
|
const response = await fetch(options.url, {method: 'POST', headers: options.headers || {}, body: formData});
|
||||||
|
if(!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
rej(JSONAttemptParse(error));
|
||||||
|
} else {
|
||||||
|
const result = await response.text();
|
||||||
|
res(<T>JSONAttemptParse(result));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
rej(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,8 +76,18 @@ export class PathEvent {
|
|||||||
/** Whether this path contains glob patterns */
|
/** Whether this path contains glob patterns */
|
||||||
hasGlob!: boolean;
|
hasGlob!: boolean;
|
||||||
|
|
||||||
/** Internal cache for PathEvent instances to avoid redundant parsing */
|
/** Internal cache for parsed path data (plain objects, not instances) */
|
||||||
private static pathEventCache: Map<string, PathEvent> = new Map();
|
private static pathEventCache: Map<string, {
|
||||||
|
module: string;
|
||||||
|
fullPath: string;
|
||||||
|
dir: string;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
methods: Method[];
|
||||||
|
hasGlob: boolean;
|
||||||
|
}> = new Map();
|
||||||
|
/** Max size for path cache before LRU eviction */
|
||||||
|
private static readonly MAX_PATH_CACHE_SIZE = 1000;
|
||||||
/** Cache for compiled permissions (path + required permissions → result) */
|
/** Cache for compiled permissions (path + required permissions → result) */
|
||||||
private static permissionCache: Map<string, PathEvent> = new Map();
|
private static permissionCache: Map<string, PathEvent> = new Map();
|
||||||
/** Max size for permission cache before LRU eviction */
|
/** Max size for permission cache before LRU eviction */
|
||||||
@@ -111,8 +121,20 @@ export class PathEvent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check cache and reconstruct from plain object
|
||||||
if(PathEvent.pathEventCache.has(e)) {
|
if(PathEvent.pathEventCache.has(e)) {
|
||||||
Object.assign(this, PathEvent.pathEventCache.get(e)!);
|
const cached = PathEvent.pathEventCache.get(e)!;
|
||||||
|
// Move to end (LRU - most recently used)
|
||||||
|
PathEvent.pathEventCache.delete(e);
|
||||||
|
PathEvent.pathEventCache.set(e, cached);
|
||||||
|
|
||||||
|
this.module = cached.module;
|
||||||
|
this.fullPath = cached.fullPath;
|
||||||
|
this.dir = cached.dir;
|
||||||
|
this.path = cached.path;
|
||||||
|
this.name = cached.name;
|
||||||
|
this.methods = new ASet(cached.methods);
|
||||||
|
this.hasGlob = cached.hasGlob;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,19 +150,49 @@ export class PathEvent {
|
|||||||
this.name = '';
|
this.name = '';
|
||||||
this.methods = new ASet<Method>(p === '*' ? ['*'] : <any>method.split(''));
|
this.methods = new ASet<Method>(p === '*' ? ['*'] : <any>method.split(''));
|
||||||
this.hasGlob = true;
|
this.hasGlob = true;
|
||||||
PathEvent.pathEventCache.set(e, this);
|
|
||||||
|
// LRU eviction
|
||||||
|
if(PathEvent.pathEventCache.size >= PathEvent.MAX_PATH_CACHE_SIZE) {
|
||||||
|
const firstKey = PathEvent.pathEventCache.keys().next().value;
|
||||||
|
if(firstKey) PathEvent.pathEventCache.delete(firstKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathEvent.pathEventCache.set(e, {
|
||||||
|
module: this.module,
|
||||||
|
fullPath: this.fullPath,
|
||||||
|
dir: this.dir,
|
||||||
|
path: this.path,
|
||||||
|
name: this.name,
|
||||||
|
methods: [...this.methods],
|
||||||
|
hasGlob: this.hasGlob
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let temp = p.split('/').filter(p => !!p);
|
let temp = p.split('/').filter(p => !!p);
|
||||||
this.module = temp.splice(0, 1)[0] || '';
|
this.module = temp.splice(0, 1)[0] || '';
|
||||||
this.path = temp.join('/');
|
this.path = temp.join('/');
|
||||||
this.dir = temp.length > 2 ? temp.slice(0, -1).join('/') : '';
|
this.dir = temp.length > 1 ? temp.slice(0, -1).join('/') : '';
|
||||||
this.fullPath = `${this.module}${this.module && this.path ? '/' : ''}${this.path}`;
|
this.fullPath = `${this.module}${this.module && this.path ? '/' : ''}${this.path}`;
|
||||||
this.name = temp.pop() || '';
|
this.name = temp.pop() || '';
|
||||||
this.hasGlob = this.fullPath.includes('*');
|
this.hasGlob = this.fullPath.includes('*');
|
||||||
this.methods = new ASet(<any>method.split(''));
|
this.methods = new ASet(<any>method.split(''));
|
||||||
PathEvent.pathEventCache.set(e, this);
|
|
||||||
|
// LRU eviction
|
||||||
|
if(PathEvent.pathEventCache.size >= PathEvent.MAX_PATH_CACHE_SIZE) {
|
||||||
|
const firstKey = PathEvent.pathEventCache.keys().next().value;
|
||||||
|
if(firstKey) PathEvent.pathEventCache.delete(firstKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathEvent.pathEventCache.set(e, {
|
||||||
|
module: this.module,
|
||||||
|
fullPath: this.fullPath,
|
||||||
|
dir: this.dir,
|
||||||
|
path: this.path,
|
||||||
|
name: this.name,
|
||||||
|
methods: [...this.methods],
|
||||||
|
hasGlob: this.hasGlob
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -241,7 +241,6 @@ export function snakeCase(str?: string): string {
|
|||||||
return wordSegments(str).map(w => w.toLowerCase()).join("_");
|
return wordSegments(str).map(w => w.toLowerCase()).join("_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splice a string together (Similar to Array.splice)
|
* Splice a string together (Similar to Array.splice)
|
||||||
*
|
*
|
||||||
@@ -257,6 +256,20 @@ export function strSplice(str: string, start: number, deleteCount: number, inser
|
|||||||
return before + insert + after;
|
return before + insert + after;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function titleCase(str: string) {
|
||||||
|
// Normalize separators: replace underscores and hyphens with spaces
|
||||||
|
let normalizedStr = str.replace(/(_|-)/g, ' ');
|
||||||
|
// Handle CamelCase/PascalCase boundaries: insert a space before capital letters
|
||||||
|
normalizedStr = normalizedStr.replace(/([a-z])([A-Z])/g, '$1 $2');
|
||||||
|
// Lowercase the whole string, split by any whitespace, and capitalize each word
|
||||||
|
let words = normalizedStr.toLowerCase().split(/\s+/).filter(Boolean);
|
||||||
|
const titledWords = words.map(word => {
|
||||||
|
if (word.length === 0) return '';
|
||||||
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||||
|
});
|
||||||
|
return titledWords.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all substrings that match a given pattern.
|
* Find all substrings that match a given pattern.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export function formatDate(format: string = 'YYYY-MM-DD H:mm', date: Date | numb
|
|||||||
});
|
});
|
||||||
|
|
||||||
const monthValue = parseInt(partsMap.month) - 1;
|
const monthValue = parseInt(partsMap.month) - 1;
|
||||||
const dayOfWeekValue = new Date(`${partsMap.year}-${partsMap.month}-${partsMap.day}`).getDay();
|
const dayOfWeekValue = new Date(Date.UTC(parseInt(partsMap.year), parseInt(partsMap.month) - 1, parseInt(partsMap.day))).getUTCDay();
|
||||||
const hourValue = parseInt(partsMap.hour);
|
const hourValue = parseInt(partsMap.hour);
|
||||||
|
|
||||||
get = (fn: 'FullYear' | 'Month' | 'Date' | 'Day' | 'Hours' | 'Minutes' | 'Seconds' | 'Milliseconds'): number => {
|
get = (fn: 'FullYear' | 'Month' | 'Date' | 'Day' | 'Hours' | 'Minutes' | 'Seconds' | 'Milliseconds'): number => {
|
||||||
|
|||||||
Reference in New Issue
Block a user