Initial commit

This commit is contained in:
Warky 2024-12-03 22:52:21 +02:00
parent 986458dd4b
commit aba68a3c0a
43 changed files with 14905 additions and 1 deletions

11
.changeset/config.json Normal file
View File

@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
node_modules/
dist/

View File

@ -1,2 +1,37 @@
# artemis-kit
Artemis Kit is a libary for various utilities and functions in javascript/typescript.
A comprehensive TypeScript/JavaScript utility library focused on precision and efficiency.
## Features
### String Tools
- Advanced string manipulation
- Pattern matching and validation
- Text transformation utilities
- Unicode handling
### Blob Tools
- Blob creation and modification
- Binary data handling
- Stream processing
- MIME type management
### File Conversion Tools
- Format conversions
- File type transformations
- Encoding/decoding utilities
- Batch processing capabilities
## Installation
```bash
pnpm install @warkypublic/artemis-kit
```
## Contributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
## License
MIT

20
eslint.config.js Normal file
View File

@ -0,0 +1,20 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
/** @type {import('eslint').Linter.Config[]} */
export default [
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
eqeqeq: "warn",
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
},
},
];

67
package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "@warkypublic/artemis-kit",
"version": "1.0.0",
"description": "A comprehensive TypeScript/JavaScript utility library focused on precision and efficiency.",
"type": "module",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
"publishConfig": {
"access": "public",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"files": [
"dist",
"bin",
"README.md"
],
"scripts": {
"build": "vite build",
"clean": "rm -rf dist",
"prepublishOnly": "npm run build",
"test": "vitest run",
"lint": "eslint src"
},
"keywords": [
"string",
"blob",
"dependencies",
"workspace",
"package",
"cli",
"tools",
"npm",
"yarn",
"pnpm"
],
"author": "Hein (Warkanum) Puth",
"license": "MIT",
"dependencies": {
"semver": "^7.6.3"
},
"devDependencies": {
"@changesets/cli": "^2.27.10",
"@eslint/js": "^9.16.0",
"eslint": "^9.16.0",
"globals": "^15.13.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.17.0",
"vite": "^6.0.2",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.1.8"
},
"engines": {
"node": ">=14.16"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Warky-Devs/artemis-kit"
},
"bugs": {
"url": "https://github.com/Warky-Devs/artemis-kit/issues"
},
"homepage": "https://github.com/Warky-Devs/artemis-kit#readme",
"packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
}

3203
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
/**
* Converts a base64 string to a Blob object.
* @param base64 - The base64 encoded string to convert
* @returns A Blob containing the decoded binary data
*/
export function base64ToBlob(base64: string): Blob {
const byteString = atob(base64)
const arrayBuffer = new ArrayBuffer(byteString.length)
const uint8Array = new Uint8Array(arrayBuffer)
for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i)
}
return new Blob([arrayBuffer])
}

View File

@ -0,0 +1,19 @@
/**
* Converts a Blob object to a base64 encoded string.
* @param blob - The Blob object to convert
* @returns Promise that resolves with the base64 encoded string
*/
function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const dataUrl = (reader.result ?? '') as string
const base64 = dataUrl?.split?.(',')?.[1]
resolve(base64)
}
reader.onerror = reject
reader.readAsDataURL(blob)
})
}
export { blobToBase64 }

View File

@ -0,0 +1,14 @@
/**
* Converts a Blob object to a string. Defaults to a blank string if the Blob is null.
* @param blob - The Blob object to convert
* @returns Promise that resolves with the text
*/
async function blobToString(blob: Blob | string): Promise<string> {
if (!blob) return ''
if (typeof blob === 'string') {
return blob
}
return await blob.text()
}
export { blobToString }

View File

@ -0,0 +1,18 @@
/**
* Converts a File object to a base64 encoded string.
* @param file - The File object to convert
* @returns Promise that resolves with the base64 encoded string
*/
function FileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const d = reader.result?.toString()
resolve(btoa(d ?? ''))
}
reader.onerror = reject
reader.readAsArrayBuffer(file)
})
}
export { FileToBase64 }

17
src/base64/FileToBlob.ts Normal file
View File

@ -0,0 +1,17 @@
/**
* Converts a File object to a Blob object.
* @param file - The File object to convert
* @returns Promise that resolves with the Blob
*/
function FileToBlob(file: File): Promise<Blob> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
resolve(new Blob([reader.result as ArrayBuffer]))
}
reader.onerror = reject
reader.readAsArrayBuffer(file)
})
}
export { FileToBlob }

View File

@ -0,0 +1,20 @@
/**
* Decodes a base64 string that contains Unicode characters.
* This function handles Unicode characters correctly by:
* 1. Converting base64 to binary
* 2. Converting each byte to a hex representation
* 3. Converting hex-encoded UTF-8 sequences back to Unicode characters
*
* @param str - The base64 encoded string to decode
* @returns The decoded Unicode string
*/
export function b64DecodeUnicode(str: string): any {
return decodeURIComponent(
Array.prototype.map
.call(
atob(str),
(c) => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`
)
.join('')
)
}

View File

@ -0,0 +1,17 @@
/**
* Encodes a Unicode string to base64.
* This function handles Unicode characters correctly by:
* 1. Converting Unicode to UTF-8 percent-encoding
* 2. Converting percent-encoded bytes to binary
* 3. Converting binary to base64
*
* @param str - The Unicode string to encode
* @returns The base64 encoded string
*/
export function b64EncodeUnicode(str: any): string {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_match, p1) =>
String.fromCharCode(Number.parseInt(p1, 16))
)
)
}

6
src/base64/index.ts Normal file
View File

@ -0,0 +1,6 @@
export { base64ToBlob } from './Base64ToBlob'
export { blobToBase64 } from './BlobToBase64'
export { FileToBase64 } from './FileToBase64'
export { FileToBlob } from './FileToBlob'
export { b64DecodeUnicode } from './base64-decode-unicode'
export { b64EncodeUnicode } from './base64-encode-unicode'

164
src/dataqueue/buffer.ts Normal file
View File

@ -0,0 +1,164 @@
// buffer.ts
import { ActiveRecord, BufferOptions } from './types';
import { NestedQueue } from './queue';
export class ActiveRecordBuffer<T extends object> {
private buffer: Map<string | number, ActiveRecord<T>>;
private queue: NestedQueue<T>;
private autoSaveTimer?: any;
private options: Required<BufferOptions>;
constructor(queue: NestedQueue<T>, options: BufferOptions = {}) {
this.queue = queue;
this.buffer = new Map();
this.options = {
autoSave: options.autoSave ?? true,
bufferSize: options.bufferSize ?? 100,
flushInterval: options.flushInterval ?? 5000
};
if (this.options.autoSave) {
this.startAutoSave();
}
}
private startAutoSave() {
this.autoSaveTimer = setInterval(() => {
this.flush();
}, this.options.flushInterval);
}
private stopAutoSave() {
if (this.autoSaveTimer) {
clearInterval(this.autoSaveTimer);
}
}
private getRecordId(record: T): string | number {
if ('id' in record) return record['id'] as string | number;
if ('key' in record) return record['key'] as string;
throw new Error('Record must have an id or key property');
}
async load(record: T): Promise<ActiveRecord<T>> {
const id = this.getRecordId(record);
if (this.buffer.has(id)) {
return this.buffer.get(id)!;
}
const activeRecord: ActiveRecord<T> = {
data: { ...record },
id,
isDirty: false,
isNew: false,
changes: {},
originalData: { ...record }
};
this.buffer.set(id, activeRecord);
if (this.buffer.size > this.options.bufferSize) {
await this.flush();
}
return activeRecord;
}
create(data: T): ActiveRecord<T> {
const id = this.getRecordId(data);
const activeRecord: ActiveRecord<T> = {
data: { ...data } as any,
id,
isDirty: true,
isNew: true,
changes: { ...data } as any,
originalData: { }
} as ActiveRecord<T>;
this.buffer.set(id, activeRecord);
return activeRecord;
}
update(id: string | number, changes: Partial<T>): ActiveRecord<T> {
const record = this.buffer.get(id);
if (!record) {
throw new Error(`Record with id ${id} not found in buffer`);
}
record.isDirty = true;
record.data = { ...record.data, ...changes };
record.changes = { ...record.changes, ...changes };
return record;
}
async delete(id: string | number) {
const record = this.buffer.get(id);
if (record) {
await this.queue.remove(String(id));
this.buffer.delete(id);
}
}
get(id: string | number): ActiveRecord<T> | undefined {
return this.buffer.get(id);
}
hasChanges(id: string | number): boolean {
const record = this.buffer.get(id);
return record ? record.isDirty : false;
}
getDirtyRecords(): ActiveRecord<T>[] {
return Array.from(this.buffer.values()).filter(record => record.isDirty);
}
getAll(): ActiveRecord<T>[] {
return Array.from(this.buffer.values());
}
async flush(): Promise<void> {
const dirtyRecords = this.getDirtyRecords();
for (const record of dirtyRecords) {
if (record.isNew) {
await this.queue.add(record.data);
} else {
await this.queue.update(String(record.id), record.changes);
}
record.isDirty = false;
record.isNew = false;
record.changes = {};
record.originalData = { ...record.data };
}
this.buffer.clear();
}
rollback(id: string | number) {
const record = this.buffer.get(id);
if (record) {
record.data = { ...record.originalData };
record.isDirty = false;
record.changes = {};
}
}
rollbackAll() {
for (const [id] of this.buffer) {
this.rollback(id);
}
}
clear() {
this.buffer.clear();
}
dispose() {
this.stopAutoSave();
this.clear();
}
}

150
src/dataqueue/examples.ts Normal file
View File

@ -0,0 +1,150 @@
// // examples.ts
// import { EnhancedNestedQueue } from './queue';
// import { LocalStoragePersistence } from './persistence';
// import { useQueue, useActiveRecord } from './hooks';
// // Example 1: Basic Queue Usage
// interface TodoItem {
// id: number;
// title: string;
// completed: boolean;
// subTasks?: TodoItem[];
// }
// const todoQueue = new EnhancedNestedQueue<TodoItem>([], {
// persistence: new LocalStoragePersistence('todos'),
// middleware: [
// {
// beforeAction: (action) => {
// console.log('Action:', action);
// return action;
// },
// afterAction: (action, state) => {
// console.log('New State:', state);
// }
// }
// ]
// });
// // Example 2: React Component with Queue
// function TodoList() {
// const { data, queue, buffer } = useQueue<TodoItem>([], {
// persistence: new LocalStoragePersistence('todos')
// });
// const addTodo = (title: string) => {
// const newTodo = buffer.create({
// id: Date.now(),
// title,
// completed: false
// });
// };
// const toggleTodo = (id: number) => {
// const todo = data.find(t => t.id === id);
// if (todo) {
// buffer.update(id, { completed: !todo.completed });
// }
// };
// return (
// <div>
// <button onClick={() => addTodo('New Task')}>Add Task</button>
// {data.map(todo => (
// <div key={todo.id}>
// <input
// type="checkbox"
// checked={todo.completed}
// onChange={() => toggleTodo(todo.id)}
// />
// {todo.title}
// </div>
// ))}
// </div>
// );
// }
// // Example 3: Active Record Pattern
// function TodoEditor({ todoId }: { todoId: number }) {
// const { data, queue } = useQueue<TodoItem>([]);
// const todo = data.find(t => t.id === todoId);
// const {
// record,
// update,
// save,
// rollback,
// isDirty
// } = useActiveRecord(queue, todo!);
// if (!record) return null;
// return (
// <div>
// <input
// value={record.data.title}
// onChange={e => update({ title: e.target.value })}
// />
// <button onClick={save} disabled={!isDirty}>
// Save
// </button>
// <button onClick={rollback} disabled={!isDirty}>
// Cancel
// </button>
// </div>
// );
// }
// // Example 4: Complex Nested Data
// interface Department {
// id: number;
// name: string;
// employees: Employee[];
// }
// interface Employee {
// id: number;
// name: string;
// roles: string[];
// }
// const departmentQueue = new EnhancedNestedQueue<Department>();
// // Adding nested data
// departmentQueue.add({
// id: 1,
// name: 'Engineering',
// employees: [
// { id: 1, name: 'John', roles: ['dev'] }
// ]
// }, '');
// // Updating nested employee
// departmentQueue.update('0.employees.0', {
// roles: ['dev', 'lead']
// });
// // Sorting employees by name
// departmentQueue.sort('name', {
// path: '0.employees',
// direction: 'asc'
// });
// // Example 5: Batch Operations
// async function batchUpdate() {
// const buffer = departmentQueue.getBuffer();
// // Load multiple records
// const records = await Promise.all([
// buffer.load({ id: 1, name: 'Dept 1', employees: [] }),
// buffer.load({ id: 2, name: 'Dept 2', employees: [] })
// ]);
// // Update multiple records
// records.forEach(record => {
// buffer.update(record.id, { name: `Updated ${record.data.name}` });
// });
// // Flush all changes at once
// await buffer.flush();
// }

84
src/dataqueue/hooks.ts Normal file
View File

@ -0,0 +1,84 @@
// // hooks.ts
// import { useState, useEffect, useRef } from 'react';
// import { EnhancedNestedQueue } from './queue';
// import { QueueOptions, BufferOptions, ActiveRecord } from './types';
// export function useQueue<T extends object>(
// initialData: T[],
// options: QueueOptions<T> = {},
// bufferOptions: BufferOptions = {}
// ) {
// const [data, setData] = useState<T[]>(initialData);
// const queueRef = useRef<EnhancedNestedQueue<T>>();
// useEffect(() => {
// if (!queueRef.current) {
// queueRef.current = new EnhancedNestedQueue<T>(
// initialData,
// options,
// bufferOptions
// );
// }
// const unsubscribe = queueRef.current.subscribe((newData) => {
// setData(newData);
// });
// return () => {
// unsubscribe();
// queueRef.current?.getBuffer().dispose();
// };
// }, []);
// return {
// data,
// queue: queueRef.current!,
// buffer: queueRef.current?.getBuffer()
// };
// }
// export function useActiveRecord<T extends object>(
// queue: EnhancedNestedQueue<T>,
// record: T
// ) {
// const [activeRecord, setActiveRecord] = useState<ActiveRecord<T>>();
// const buffer = queue.getBuffer();
// useEffect(() => {
// buffer.load(record).then(setActiveRecord);
// return () => {
// if (activeRecord?.isDirty) {
// buffer.flush();
// }
// };
// }, [record]);
// const updateRecord = (changes: Partial<T>) => {
// if (activeRecord) {
// const updated = buffer.update(activeRecord.id, changes);
// setActiveRecord(updated);
// }
// };
// const saveRecord = async () => {
// if (activeRecord?.isDirty) {
// await buffer.flush();
// }
// };
// const rollbackRecord = () => {
// if (activeRecord) {
// buffer.rollback(activeRecord.id);
// setActiveRecord(buffer.get(activeRecord.id));
// }
// };
// return {
// record: activeRecord,
// update: updateRecord,
// save: saveRecord,
// rollback: rollbackRecord,
// isDirty: activeRecord?.isDirty ?? false
// };
// }

9
src/dataqueue/index.ts Normal file
View File

@ -0,0 +1,9 @@
export {NestedQueue,EnhancedNestedQueue} from './queue'
import {InMemoryPersistence,IndexedDBPersistence,LocalStoragePersistence} from './persistence'
const NestedQueuePersistence = {
InMemoryPersistence,
IndexedDBPersistence,
LocalStoragePersistence
}
export {NestedQueuePersistence}

View File

@ -0,0 +1,99 @@
import type { PersistenceAdapter, QueueData } from './types';
export class LocalStoragePersistence<T> implements PersistenceAdapter<T> {
constructor(private key: string) {
this.key = key;
}
async save(data: QueueData<T>): Promise<void> {
localStorage.setItem(this.key, JSON.stringify(data));
}
async load(): Promise<QueueData<T> | null> {
const data = localStorage.getItem(this.key);
return data ? JSON.parse(data) : null;
}
async clear(): Promise<void> {
localStorage.removeItem(this.key);
}
}
export class IndexedDBPersistence<T> implements PersistenceAdapter<T> {
private dbName: string;
private storeName: string;
constructor(dbName: string, storeName: string) {
this.dbName = dbName;
this.storeName = storeName;
}
private openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}
async save(data: QueueData<T>): Promise<void> {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put(data, 'queueData');
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async load(): Promise<QueueData<T> | null> {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get('queueData');
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result || null);
});
}
async clear(): Promise<void> {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.clear();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
}
export class InMemoryPersistence<T> implements PersistenceAdapter<T> {
private data: QueueData<T> | null = null;
async save(data: QueueData<T>): Promise<void> {
this.data = [...data];
}
async load(): Promise<QueueData<T> | null> {
return this.data ? [...this.data] : null;
}
async clear(): Promise<void> {
this.data = null;
}
}

285
src/dataqueue/queue.ts Normal file
View File

@ -0,0 +1,285 @@
// queue.ts
import {
QueueData,
QueueOptions,
QueueAction,
SortOptions,
BufferOptions,
Middleware,
PersistenceAdapter,
} from "./types";
import { ActiveRecordBuffer } from "./buffer";
import { QueueUtils } from "./utils";
export class NestedQueue<T extends object> {
protected data: QueueData<T>;
private listeners: Set<(data: QueueData<T>) => void>;
private middleware: Middleware<T>[];
private persistence?: PersistenceAdapter<T>;
private initialized: Promise<void>;
constructor(initialData: QueueData<T> = [], options: QueueOptions<T> = {}) {
this.data = [...initialData];
this.listeners = new Set();
this.middleware = options.middleware || [];
this.persistence = options.persistence;
this.initialized =
options.autoload && this.persistence
? this.loadFromPersistence()
: Promise.resolve();
}
subscribe(callback: () => void) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
private notify() {
this.listeners.forEach((callback) => callback(this.data));
}
private getNestedValue(obj: any, path: string | string[]) {
const keys = Array.isArray(path) ? path : path.split(".");
return keys.reduce((current, key) => current?.[key], obj);
}
private setNestedValue(obj: any, path: string, value: any) {
const keys = path.split(".");
const lastKey = keys.pop()!;
const target = keys.reduce((current, key) => {
current[key] = current[key] || {};
return current[key];
}, obj);
target[lastKey] = value;
return obj;
}
private async executeAction(action: QueueAction<T>): Promise<void> {
// Run through beforeAction middleware
let currentAction = action;
for (const m of this.middleware) {
if (m.beforeAction) {
currentAction = m.beforeAction(currentAction) || currentAction;
if (!currentAction) return; // Action cancelled by middleware
}
}
// Execute the action
switch (currentAction.type) {
case "add":
this.executeAdd(currentAction.payload, currentAction.path);
break;
case "remove":
this.executeRemove(currentAction.path!);
break;
case "update":
this.executeUpdate(currentAction.path!, currentAction.payload);
break;
case "sort":
this.executeSort(
currentAction.payload.key,
currentAction.payload.options
);
break;
case "clear":
this.executeClear();
break;
}
// Run through afterAction middleware
for (const m of this.middleware) {
if (m.afterAction) {
m.afterAction(currentAction, this.data);
}
}
// Persist changes if enabled
if (this.persistence) {
await this.persistence.save(this.data);
}
// Notify listeners
this.notify();
}
private executeAdd(item: T, path: string = "") {
this.data = [...this.data];
if (!path) {
this.data.push(item);
return;
}
const target = this.getNestedValue(this.data, path);
if (Array.isArray(target)) {
target.push(item);
}
}
private executeRemove(path: string) {
const pathParts = path.split(".");
const index = pathParts.pop()!;
const parentPath = pathParts.join(".");
this.data = [...this.data];
const target = parentPath
? this.getNestedValue(this.data, parentPath)
: this.data;
if (Array.isArray(target)) {
target.splice(Number(index), 1);
}
}
private executeUpdate(path: string, value: Partial<T>) {
this.data = [...this.data];
const existing = this.getNestedValue(this.data, path);
if (existing) {
this.setNestedValue(this.data, path, { ...existing, ...value });
}
}
private executeSort(key: keyof T | string[], options: SortOptions<T> = {}) {
const {
deep = true,
maxDepth = Infinity,
path = "",
direction = "asc",
sortFn,
} = options;
const sortArray = (array: any[], depth: number = 0): void => {
if (sortFn) {
array.sort((a, b) => sortFn(a, b));
} else {
array.sort((a, b) => {
const aValue = Array.isArray(key)
? this.getNestedValue(a, key)
: a[key];
const bValue = Array.isArray(key)
? this.getNestedValue(b, key)
: b[key];
return QueueUtils.compareValues(aValue, bValue, direction);
});
}
if (deep && depth < maxDepth) {
array.forEach((item) => {
Object.values(item).forEach((value) => {
if (Array.isArray(value)) {
sortArray(value, depth + 1);
}
});
});
}
};
this.data = [...this.data];
if (path) {
const target = this.getNestedValue(this.data, path);
if (Array.isArray(target)) {
sortArray(target);
}
} else {
sortArray(this.data);
}
}
private executeClear() {
this.data = [];
}
// ... Core action execution methods ...
// (Previously defined methods: executeAction, executeAdd, executeRemove, etc.)
// Public API methods
async add(item: T, path: string = "") {
await this.initialized;
return this.executeAction({
type: "add",
payload: item,
path,
});
}
async remove(path: string) {
await this.initialized;
return this.executeAction({
type: "remove",
path,
});
}
async update(path: string, value: Partial<T>) {
await this.initialized;
return this.executeAction({
type: "update",
payload: value,
path,
});
}
get(path: string) {
return this.getNestedValue(this.data, path);
}
getAll() {
return this.data;
}
// ... Search and filter methods ...
// (Previously defined methods: search, filter, findOne, sort)
async clear() {
await this.initialized;
return this.executeAction({
type: "clear",
});
}
private async loadFromPersistence(): Promise<void> {
if (!this.persistence) return;
try {
const persistedData = await this.persistence.load();
if (persistedData) {
this.data = persistedData;
this.notify();
}
} catch (error) {
console.error("Failed to load persisted data:", error);
}
}
async clearPersistence(): Promise<void> {
if (this.persistence) {
await this.persistence.clear();
}
}
}
// Enhanced queue with buffer support
export class EnhancedNestedQueue<T extends object> extends NestedQueue<T> {
private activeRecordBuffer: ActiveRecordBuffer<T>;
constructor(
initialData: T[] = [],
options: QueueOptions<T> = {},
bufferOptions: BufferOptions = {}
) {
super(initialData, options);
this.activeRecordBuffer = new ActiveRecordBuffer<T>(this, bufferOptions);
}
getBuffer(): ActiveRecordBuffer<T> {
return this.activeRecordBuffer;
}
}
export default EnhancedNestedQueue;

73
src/dataqueue/types.ts Normal file
View File

@ -0,0 +1,73 @@
export type QueueData<T> = T[];
export interface SearchOptions {
exact?: boolean;
caseSensitive?: boolean;
deep?: boolean;
paths?: string[];
}
export interface FilterOptions {
deep?: boolean;
maxDepth?: number;
}
export interface SortOptions<T> {
deep?: boolean;
maxDepth?: number;
path?: string;
direction?: 'asc' | 'desc';
sortFn?: (a: T, b: T) => number;
}
export type QueueAction<T> = {
type: 'add' | 'remove' | 'update' | 'sort' | 'clear';
payload?: T | any;
path?: string;
};
export interface Middleware<T> {
beforeAction?: (action: QueueAction<T>) => QueueAction<T> | null;
afterAction?: (action: QueueAction<T>, state: QueueData<T>) => void;
}
export interface PersistenceAdapter<T> {
save: (data: QueueData<T>) => Promise<void>;
load: () => Promise<QueueData<T> | null>;
clear: () => Promise<void>;
}
export interface QueueOptions<T> {
persistence?: PersistenceAdapter<T>;
middleware?: Middleware<T>[];
autoload?: boolean;
}
export interface BufferOptions {
autoSave?: boolean;
bufferSize?: number;
flushInterval?: number;
}
export interface ActiveRecord<T> {
data: T;
id: string | number;
isDirty: boolean;
isNew: boolean;
changes: Partial<T>;
originalData: T;
}
export interface QueueOperations<T> {
add: (item: T, path?: string) => Promise<void>;
remove: (path: string) => Promise<void>;
update: (path: string, value: Partial<T>) => Promise<void>;
get: (path: string) => any;
getAll: () => T[];
search: (query: string | object, options?: SearchOptions) => T[];
filter: (predicate: (item: T) => boolean, options?: FilterOptions) => T[];
findOne: (predicate: (item: T) => boolean) => T | undefined;
sort: (key: keyof T | string[], options?: SortOptions<T>) => Promise<void>;
clear: () => Promise<void>;
}

117
src/dataqueue/utils.ts Normal file
View File

@ -0,0 +1,117 @@
// utils.ts
export class QueueUtils {
static deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => QueueUtils.deepClone(item)) as unknown as T;
}
return Object.keys(obj).reduce((acc, key) => {
acc[key] = QueueUtils.deepClone(obj[key]);
return acc;
}, {} as T);
}
static createPath(parts: (string | number)[]): string {
return parts.join('.');
}
static parsePath(path: string): string[] {
return path.split('.');
}
static getValueAtPath<T>(obj: T, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
static setValueAtPath<T>(obj: T, path: string, value: any): T {
const clone = QueueUtils.deepClone(obj);
const keys = path.split('.');
const lastKey = keys.pop()!;
const target = keys.reduce((current:any, key) => {
if (!(key in current)) {
current[key] = {};
}
return current[key];
}, clone);
target[lastKey] = value;
return clone;
}
static compareValues(a: any, b: any, direction: 'asc' | 'desc' = 'asc'): number {
if (a === b) return 0;
if (a === null) return direction === 'asc' ? -1 : 1;
if (b === null) return direction === 'asc' ? 1 : -1;
const typeA = typeof a;
const typeB = typeof b;
if (typeA !== typeB) {
a = String(a);
b = String(b);
}
if (typeA === 'string') {
return direction === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
}
return direction === 'asc' ? a - b : b - a;
}
static debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: any;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
static throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
static batch(callback: () => void): void {
Promise.resolve().then(callback);
}
}
// Decorators for method enhancement
export function debounce(wait: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = QueueUtils.debounce(original, wait);
return descriptor;
};
}
export function throttle(limit: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = QueueUtils.throttle(original, limit);
return descriptor;
};
}

24
src/i18n/examples.ts Normal file
View File

@ -0,0 +1,24 @@
/**
* Example usage:
* ```typescript
* import { i18n, t, tt } from './i18n';
*
* // Configure once at app startup
* i18n.configure({
* apiUrl: 'https://api.example.com/translations'
* });
*
* // Sync usage
* const message = t('welcome_message', 'Welcome');
*
* // Async usage
* const asyncMessage = await tt('welcome_message', 'Welcome');
*
* // Listen for updates
* window.addEventListener('i18n-updated', (event) => {
* const { id, value } = event.detail;
* // Update UI with new translation
* });
* ```
*/

356
src/i18n/index.ts Normal file
View File

@ -0,0 +1,356 @@
/**
* @fileoverview Internationalization module with IndexedDB caching and server synchronization
* @version 1.0.0
*/
/**
* Configuration options for the I18n manager
*/
type I18nConfig = {
/** Base URL for the translations API */
apiUrl?: string;
};
/**
* Translation entry structure as stored in the cache
*/
type CacheEntry = {
/** The translated string value */
value: string;
/** Version number of the translation */
version: number;
};
/**
* Translation string registration format
*/
type TranslationString = {
/** Unique identifier for the translation */
id: string;
/** The translated text */
value: string;
};
/**
* Server response format for translation requests
*/
type TranslationResponse = {
/** The translated text */
value: string;
/** Version number of the translation */
version: number;
};
/**
* Event payload for translation updates
*/
type I18nUpdateEvent = CustomEvent<{
/** Identifier of the updated translation */
id: string;
/** New translated value */
value: string;
}>;
/**
* Core I18n manager interface
*/
interface I18nManager {
configure(options?: I18nConfig): void;
registerStrings(strings: TranslationString[], version: number): Promise<void>;
getString(componentId: string, defaultValue?: string): Promise<string>;
getStringSync(componentId: string, defaultValue?: string): string;
clearCache(): Promise<void>;
getApiUrl(): string;
}
const createI18nManager = (): I18nManager => {
/** Database name for IndexedDB storage */
const DB_NAME = 'i18n-cache';
/** Store name for translations within IndexedDB */
const STORE_NAME = 'translations';
/** Current version of the translations schema */
const CURRENT_VERSION = 1;
/** Default API endpoint if none provided */
const DEFAULT_API_URL = '/api/translations';
let db: IDBDatabase | null = null;
let serverUrl: string = DEFAULT_API_URL;
const cache = new Map<string, CacheEntry>();
const pendingUpdates = new Set<string>();
let initPromise: Promise<void> | null = null;
let isInitialized = false;
/**
* Initializes the IndexedDB database and sets up the object store
* @returns Promise that resolves when the database is ready
*/
const initDatabase = async (): Promise<void> => {
if (isInitialized) return;
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, CURRENT_VERSION);
request.onerror = () => reject(new Error('Failed to open database'));
request.onupgradeneeded = (event) => {
const database = (event.target as IDBOpenDBRequest).result;
if (!database.objectStoreNames.contains(STORE_NAME)) {
const store = database.createObjectStore(STORE_NAME, { keyPath: 'id' });
store.createIndex('version', 'version', { unique: false });
}
};
request.onsuccess = async (event) => {
db = (event.target as IDBOpenDBRequest).result;
await loadCacheFromDb();
isInitialized = true;
resolve();
};
});
};
/**
* Loads all translations from IndexedDB into memory cache
*/
const loadCacheFromDb = async (): Promise<void> => {
if (!db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
if (!db) throw new Error('Database not initialized');
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = (event) => {
const entries = (event.target as IDBRequest).result;
entries.forEach(entry => {
cache.set(entry.id, {
value: entry.value,
version: entry.version
});
});
resolve();
};
request.onerror = () => reject(new Error('Failed to load cache'));
});
};
/**
* Registers new translations in both IndexedDB and memory cache
* @param strings - Array of translation strings to register
* @param version - Version number for these translations
*/
const registerStrings = async (strings: TranslationString[], version: number): Promise<void> => {
if (!initPromise) configure();
await initPromise;
if (!db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
if (!db) throw new Error('Database not initialized');
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
strings.forEach(string => {
const entry = {
id: string.id,
value: string.value,
version: version
};
store.put(entry);
cache.set(string.id, {
value: string.value,
version: version
});
});
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(new Error('Failed to register strings'));
});
};
/**
* Fetches a translation from the server
* @param componentId - Identifier for the translation to fetch
* @returns The translated string or null if fetch fails
*/
const fetchFromServer = async (componentId: string): Promise<string | null> => {
try {
const response = await fetch(`${serverUrl}/${componentId}`);
if (!response.ok) {
throw new Error('Failed to fetch translation');
}
const data: TranslationResponse = await response.json();
await registerStrings([{
id: componentId,
value: data.value
}], data.version);
return data.value;
} catch (error) {
console.error('Error fetching translation:', error);
return null;
} finally {
pendingUpdates.delete(componentId);
}
};
/**
* Emits a custom event when translations are updated
* @param id - Identifier of the updated translation
* @param value - New translated value
*/
const emitUpdateEvent = (id: string, value: string): void => {
const event = new CustomEvent('i18n-updated', {
detail: { id, value }
}) as I18nUpdateEvent;
window.dispatchEvent(event);
};
/**
* Synchronously retrieves a translation with background update check
* @param componentId - Identifier for the translation
* @param defaultValue - Fallback value if translation not found
* @returns The translated string or default value
*/
const getStringSync = (componentId: string, defaultValue = ''): string => {
if (!isInitialized) {
console.warn('I18nManager not initialized. Call configure() first or await initialization.');
return defaultValue;
}
const cached = cache.get(componentId);
if (cached) {
if (cached.version !== CURRENT_VERSION && !pendingUpdates.has(componentId)) {
pendingUpdates.add(componentId);
fetchFromServer(componentId).then(newValue => {
if (newValue) {
emitUpdateEvent(componentId, newValue);
}
});
}
return cached.value;
}
if (!pendingUpdates.has(componentId)) {
pendingUpdates.add(componentId);
fetchFromServer(componentId).then(newValue => {
if (newValue) {
emitUpdateEvent(componentId, newValue);
}
});
}
return defaultValue;
};
/**
* Asynchronously retrieves a translation
* @param componentId - Identifier for the translation
* @param defaultValue - Fallback value if translation not found
* @returns Promise resolving to the translated string
*/
const getString = async (componentId: string, defaultValue = ''): Promise<string> => {
if (!initPromise) configure();
await initPromise;
if (!db) throw new Error('Database not initialized');
return new Promise( (resolve) => {
if (!db) throw new Error('Database not initialized');
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(componentId);
request.onsuccess = async (event) => {
const result = (event.target as IDBRequest).result;
if (result && result.version === CURRENT_VERSION) {
resolve(result.value);
return;
}
const serverValue = await fetchFromServer(componentId);
resolve(serverValue || defaultValue);
};
request.onerror = () => {
console.error('Error reading from cache');
resolve(defaultValue);
};
});
};
/**
* Clears all cached translations
*/
const clearCache = async (): Promise<void> => {
if (!initPromise) configure();
await initPromise;
if (!db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
if (!db) throw new Error('Database not initialized');
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.clear();
request.onsuccess = () => {
cache.clear();
resolve();
};
request.onerror = () => reject(new Error('Failed to clear cache'));
});
};
/**
* Configures the I18n manager
* @param options - Configuration options
*/
const configure = (options: I18nConfig = {}): void => {
if (!isInitialized) {
serverUrl = options.apiUrl || DEFAULT_API_URL;
initPromise = initDatabase();
return;
}
if (options.apiUrl) {
serverUrl = options.apiUrl;
pendingUpdates.clear();
}
};
/**
* Returns the current API URL
*/
const getApiUrl = (): string => serverUrl;
return {
configure,
registerStrings,
getString,
getStringSync,
clearCache,
getApiUrl
};
};
// Create the singleton instance
const i18nManager = createI18nManager();
// Export the main manager
export const i18n = i18nManager;
// Export shortcut functions
export const _t = i18nManager.getStringSync;
export const _tt = i18nManager.getString;
// Export everything as a namespace
export default {
...i18nManager,
_t,
_tt
};
// Type declarations for the shortcut functions
export type GetStringSync = typeof i18nManager.getStringSync;
export type GetString = typeof i18nManager.getString;

8
src/index.ts Normal file
View File

@ -0,0 +1,8 @@
export * from './base64'
export * from './strings'
export * from './mime'
export * from './promise'
export * from './i18n'
export * from './dataqueue'
//export * from './logger'

43
src/logger/examples.ts Normal file
View File

@ -0,0 +1,43 @@
// // Example server reporting plugin
// const createServerReportingPlugin = (endpoint: string): LoggerPlugin => ({
// name: 'ServerReportingPlugin',
// onLog: async (entry: LogEntry): Promise<void> => {
// // Only send errors and warnings to the server
// if (entry.level === 'ERROR' || entry.level === 'WARN') {
// try {
// const response = await fetch(endpoint, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(entry)
// });
// if (!response.ok) {
// throw new Error(`HTTP error! status: ${response.status}`);
// }
// } catch (error) {
// console.error('Failed to send log to server:', error);
// }
// }
// }
// });
// // Example local storage plugin
// const createLocalStoragePlugin = (maxEntries: number = 100): LoggerPlugin => {
// const STORAGE_KEY = 'app_logs';
// return {
// name: 'LocalStoragePlugin',
// onLog: async (entry: LogEntry): Promise<void> => {
// try {
// const storedLogs = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
// const updatedLogs = [entry, ...storedLogs].slice(0, maxEntries);
// localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedLogs));
// } catch (error) {
// console.error('Failed to store log in localStorage:', error);
// }
// }
// };
// };

102
src/logger/index.ts Normal file
View File

@ -0,0 +1,102 @@
// Types
type LogLevel = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
type LogEntry = {
timestamp: Date;
level: LogLevel;
message: string;
data?: unknown;
};
type LoggerPlugin = {
name: string;
onLog: (entry: LogEntry) => Promise<void>;
};
type Logger = {
error: (message: string, data?: unknown) => Promise<void>;
warn: (message: string, data?: unknown) => Promise<void>;
info: (message: string, data?: unknown) => Promise<void>;
debug: (message: string, data?: unknown) => Promise<void>;
registerPlugin: (plugin: LoggerPlugin) => void;
};
// Create logger instance
const createLogger = (): Logger => {
const plugins: LoggerPlugin[] = [];
const getLogLevelEmoji = (level: LogLevel): string => {
switch (level) {
case 'ERROR':
return '🔥'; // Fire for errors
case 'WARN':
return '⚠️'; // Warning sign for warnings
case 'INFO':
return '💡'; // Light bulb for info
case 'DEBUG':
return '🔍'; // Magnifying glass for debug
}
};
const formatMessage = (entry: LogEntry): string => {
const timestamp = entry.timestamp.toISOString();
const emoji = getLogLevelEmoji(entry.level);
let message = `${emoji} [${timestamp}] ${entry.level}: ${entry.message}`;
if (entry.data) {
message += `\nData: ${JSON.stringify(entry.data, null, 2)}`;
}
return message;
};
const notifyPlugins = async (entry: LogEntry): Promise<void> => {
const pluginPromises = plugins.map(plugin =>
plugin.onLog(entry).catch(error => {
console.error(`Plugin "${plugin.name}" failed:`, error);
})
);
await Promise.all(pluginPromises);
};
const log = async (level: LogLevel, message: string, data?: unknown): Promise<void> => {
const entry: LogEntry = {
timestamp: new Date(),
level,
message,
data
};
const formattedMessage = formatMessage(entry);
// Console output with color coding
switch (level) {
case 'ERROR':
console.error(formattedMessage);
break;
case 'WARN':
console.warn(formattedMessage);
break;
case 'INFO':
console.info(formattedMessage);
break;
case 'DEBUG':
console.debug(formattedMessage);
break;
}
await notifyPlugins(entry);
};
return {
error: (message: string, data?: unknown) => log('ERROR', message, data),
warn: (message: string, data?: unknown) => log('WARN', message, data),
info: (message: string, data?: unknown) => log('INFO', message, data),
debug: (message: string, data?: unknown) => log('DEBUG', message, data),
registerPlugin: (plugin: LoggerPlugin) => {
plugins.push(plugin);
console.log(`🔌 Plugin "${plugin.name}" registered`);
}
};
};
export {createLogger}
export type {LogEntry,LogLevel,LoggerPlugin,Logger}

80
src/mime/index.ts Normal file
View File

@ -0,0 +1,80 @@
import { MimeTypeList } from "./mime";
/**
* Gets file extension from MIME type, with substring matching
* @param pMime - MIME type string (e.g. "image/jpeg")
* @returns First matching extension or empty string if not found
*/
export const getExtFromMime = (pMime?: string): string => {
if (!pMime) return "";
const mime = pMime.toLowerCase().trim();
// Try exact match first
const exactMatch = MimeTypeList[mime]?.extensions?.[0];
if (exactMatch) return exactMatch;
// Try substring match
const keys = Object.keys(MimeTypeList);
for (const key of keys) {
if (key.includes(mime) || mime.includes(key)) {
return MimeTypeList[key]?.extensions?.[0] ?? "";
}
}
return "";
};
/**
* Gets the file extension from a filename
* @param pFilename - Name of file including extension
* @returns Extension without dot or empty string if no extension
*/
export const getExtFromFilename = (pFilename: string): string => {
if (!pFilename) return "";
const parts = pFilename.trim().split(".");
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : "";
};
/**
* Gets the MIME type for a file extension
* @param pExt - File extension with or without dot
* @returns MIME type string or empty string if not found
*/
export const getMimeFromExt = (pExt: string): string => {
if (!pExt) return "";
pExt = pExt.replace(".", "");
const keys = Object.keys(MimeTypeList);
for (let i = 0; i < keys.length; i += 1) {
const mime = MimeTypeList[keys[i]]?.extensions?.find(
(v: string) => v.toLowerCase() === pExt.toLowerCase()
);
if (mime && mime !== "") {
return keys[i];
}
}
return "";
};
/**
* Checks if a file extension is valid for a given MIME type
* @param pMime - MIME type string
* @param pExt - File extension to check
* @returns boolean indicating if extension matches MIME type
*/
export const isValidExtForMime = (pMime: string, pExt: string): boolean => {
if (!pMime || !pExt) return false;
const mime = pMime.toLowerCase().trim();
const ext = pExt.replace(".", "").toLowerCase().trim();
return MimeTypeList[mime]?.extensions?.includes(ext) ?? false;
};
/**
* Gets all valid extensions for a MIME type
* @param pMime - MIME type string
* @returns Array of valid extensions or empty array if none found
*/
export const getAllExtensionsForMime = (pMime: string): string[] => {
if (!pMime) return [];
const mime = pMime.toLowerCase().trim();
return MimeTypeList[mime]?.extensions ?? [];
};

8878
src/mime/mime.ts Normal file

File diff suppressed because it is too large Load Diff

38
src/object/compare.ts Normal file
View File

@ -0,0 +1,38 @@
/**
* Performs object comparison with optional deep comparison
* @param obj First object to compare
* @param objToCompare Second object to compare
* @param deep Enable deep comparison for nested objects/arrays
*/
export function objectCompare<T extends Record<string, unknown>>(
obj: T,
objToCompare: T,
deep = false
): boolean {
if (!obj || !objToCompare) return false;
return Object.keys(obj).length === Object.keys(objToCompare).length &&
Object.keys(obj).every((key) => {
if (!Object.prototype.hasOwnProperty.call(objToCompare, key)) return false;
const val1 = obj[key];
const val2 = objToCompare[key];
if (!deep) return val1 === val2;
if (Array.isArray(val1) && Array.isArray(val2)) {
return val1.length === val2.length &&
val1.every((item, i) =>
typeof item === 'object' && item !== null
? objectCompare(item as Record<string, unknown>, val2[i] as Record<string, unknown>, true)
: item === val2[i]
);
}
if (typeof val1 === 'object' && typeof val2 === 'object' && val1 !== null && val2 !== null) {
return objectCompare(val1 as Record<string, unknown>, val2 as Record<string, unknown>, true);
}
return val1 === val2;
});
}

3
src/object/index.ts Normal file
View File

@ -0,0 +1,3 @@
export {getNestedValue,setNestedValue} from './nested'
export {objectCompare} from './compare'
export {createSelectOptions} from './util'

46
src/object/nested.ts Normal file
View File

@ -0,0 +1,46 @@
/**
* Gets a nested value from an object using a path string
* @param path - Dot notation path (e.g. 'user.contacts[0].email')
* @param obj - Source object to extract value from
* @returns Value at path or undefined if path invalid
*/
export function getNestedValue(path: string, obj: Record<string, any>): any {
return path
.replace(/\[(\w+)\]/g, ".$1") // Convert brackets to dot notation
.split(".") // Split path into parts
.reduce((prev, curr) => prev?.[curr], obj); // Traverse object
}
/**
* Sets a nested value in an object using a path string
* @param path - Dot notation path (e.g. 'user.contacts[0].email')
* @param value - Value to set at path
* @param obj - Target object to modify
* @returns Modified object
*/
/**
* Sets a nested value, creating objects and arrays if needed
*/
export function setNestedValue(path: string, value: any, obj: Record<string, any>): Record<string, any> {
const parts = path.replace(/\[(\w+)\]/g, ".$1").split(".");
const lastKey = parts.pop()!;
const target = parts.reduce((prev, curr) => {
// Handle array indices
if (/^\d+$/.test(curr)) {
if (!Array.isArray(prev[curr])) {
prev[curr] = [];
}
}
// Create missing objects
else if (!prev[curr]) {
prev[curr] = {};
}
return prev[curr];
}, obj);
target[lastKey] = value;
return obj;
}

49
src/object/util.ts Normal file
View File

@ -0,0 +1,49 @@
/**
* Converts an object or array into an array of key-value pairs with customizable property names.
* Commonly used for transforming data into a format suitable for select/dropdown components.
*
* @param obj - The input object or array to be transformed
* @param options - Configuration options for the transformation
* @param options.labelKey - The key name to use for the label property (default: 'label')
* @param options.valueKey - The key name to use for the value property (default: 'value')
* @returns An array of objects with the specified label and value properties, or an empty array if input is invalid
*
* @example
* // Basic usage
* createSelectOptions({ key1: "Label 1", key2: "Label 2" })
* // Returns: [{ label: "Label 1", value: "key1" }, { label: "Label 2", value: "key2" }]
*
* // Custom key names
* createSelectOptions(
* { key1: "Label 1", key2: "Label 2" },
* { labelKey: 'text', valueKey: 'id' }
* )
* // Returns: [{ text: "Label 1", id: "key1" }, { text: "Label 2", id: "key2" }]
*/
export function createSelectOptions<T = string>(
obj: Record<string, T> | T[] | null | undefined,
options: {
labelKey?: string;
valueKey?: string;
} = {}
): Array<Record<string, T | string>> {
const {
labelKey = 'label',
valueKey = 'value'
} = options;
// Return empty array for null or undefined input
if (!obj) return [];
// Return the original array if input is already an array
if (Array.isArray(obj)) return obj as Array<Record<string, T | string>>;
// Return empty array for non-object inputs (like numbers or strings)
if (typeof obj !== 'object') return [];
// Transform object into array of key-value pairs with custom property names
return Object.keys(obj).map((key) => ({
[labelKey]: obj[key],
[valueKey]: key
}));
}

84
src/promise/index.ts Normal file
View File

@ -0,0 +1,84 @@
/**
* Configuration options for the WaitUntil function
*/
interface WaitUntilOptions {
/** Maximum time to wait in milliseconds before timing out (default: 5000ms) */
timeout?: number;
/** Interval between condition checks in milliseconds (default: 100ms) */
interval?: number;
}
/**
* Asynchronously waits until a condition is met or times out
* @param condition - Function that returns true when the wait condition is satisfied
* @param options - Configuration options for timeout and check interval
* @throws {Error} When the timeout period is exceeded
* @returns Promise that resolves when the condition is met
* @example
* await WaitUntil(() => someElement.isVisible(), { timeout: 10000 });
*/
export const WaitUntil = async (
condition: () => boolean,
options?: WaitUntilOptions
): Promise<void> =>
new Promise((resolve, reject) => {
const interval = setInterval(() => {
if (!condition()) {
return;
}
clearInterval(interval);
resolve();
}, options?.interval ?? 100);
setTimeout(() => {
clearInterval(interval);
reject(new Error("Wait Timeout"));
}, options?.timeout ?? 5000);
});
/**
* Creates a debounced version of a function that delays its execution
* until after the specified wait time has elapsed since the last call
*/
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeoutId: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), wait);
};
};
/**
* Creates a throttled version of a function that executes at most once
* during the specified interval
*/
export const throttle = <T extends (...args: any[]) => any>(
func: T,
limit: number
): ((...args: Parameters<T>) => void) => {
let inThrottle = false;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};
/**
* Measures the execution time of an async function
*/
export const measureTime = async <T>(
fn: () => Promise<T>
): Promise<{ result: T; duration: number }> => {
const start = performance.now();
const result = await fn();
const duration = performance.now() - start;
return { result, duration };
};

View File

@ -0,0 +1,83 @@
/**
* Capitalizes the first letter of each word in a sentence
* @param sentence - Input string to transform
* @returns Transformed string with initial capitals
*/
export const initCaps = (sentence: string): string => {
if (!sentence) return sentence;
return sentence.replace(/(^\w{1})|(\s+\w{1})/g, letter => letter.toUpperCase());
};
/**
* Converts text to title case, following standard title capitalization rules
* Keeps articles, conjunctions, and prepositions in lowercase unless they start the title
* @param sentence - Input string to transform
* @returns Transformed string in title case
*/
export const titleCase = (sentence: string): string => {
if (!sentence) return sentence;
const smallWords = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\.?|via)$/i;
return sentence.toLowerCase().replace(/[A-Za-z0-9\u00C0-\u00FF]+[^\s-]*/g, (word, index, title) => {
if (index > 0 && index + word.length !== title.length &&
word.search(smallWords) > -1 && title.charAt(index - 2) !== ":" &&
(title.charAt(index + word.length) !== '-' || title.charAt(index - 1) === '-') &&
title.charAt(index - 1).search(/[^\s-]/) < 0) {
return word.toLowerCase();
}
return word.charAt(0).toUpperCase() + word.substr(1);
});
};
/**
* Converts text to camelCase format
* @param sentence - Input string to transform
* @returns Transformed string in camelCase
*/
export const camelCase = (sentence: string): string => {
if (!sentence) return sentence;
return sentence
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) =>
index === 0 ? letter.toLowerCase() : letter.toUpperCase())
.replace(/\s+/g, '');
};
/**
* Converts text to snake_case format
* @param sentence - Input string to transform
* @returns Transformed string in snake_case
*/
export const snakeCase = (sentence: string): string => {
if (!sentence) return sentence;
return sentence
.replace(/\s+/g, '_')
.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
.replace(/^_/, '')
.toLowerCase();
};
/**
* Converts snake_case to camelCase
* @param sentence - Input string in snake_case
* @returns Transformed string in camelCase
*/
export const reverseSnakeCase = (sentence: string): string => {
if (!sentence) return sentence;
return sentence
.toLowerCase()
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
};
/**
* Splits camelCase text into space-separated words
* @param sentence - Input string in camelCase
* @returns Space-separated string
*/
export const splitCamelCase = (sentence: string): string => {
if (!sentence) return sentence;
return sentence
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
.trim();
};

36
src/strings/fileSize.ts Normal file
View File

@ -0,0 +1,36 @@
/**
* Converts bytes to human readable file size string with units
* @param bytes - Number of bytes to format
* @param si - Use SI units (1000) instead of binary units (1024)
* @param dp - Decimal places to display
* @returns Formatted string with appropriate unit suffix
*/
export function humanFileSize(bytes: number, si: boolean = false, dp: number = 1): string {
// Define base unit (1000 for SI, 1024 for binary)
const thresh: number = si ? 1000 : 1024;
// Return bytes if below threshold
if (Math.abs(bytes) < thresh) {
return `${bytes} B`;
}
// Define unit suffixes based on SI/binary preference
const units: string[] = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u: number = -1;
const r: number = 10 ** dp;
// Divide by threshold until result is less than threshold or we run out of units
do {
bytes /= thresh;
++u;
} while (
Math.round(Math.abs(bytes) * r) / r >= thresh &&
u < units.length - 1
);
// Format with specified decimal places and appropriate unit
return `${bytes.toFixed(dp)} ${units[u]}`;
}

6
src/strings/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from './trim'
export * from './replace'
export * from './caseConversion'
export * from './locale'
export * from './fileSize'
export * from './legacy'

278
src/strings/legacy.ts Normal file
View File

@ -0,0 +1,278 @@
/**
* Checks if a value exists in an array or matches a single value
* @param args - First argument is the source (array/value), followed by values to check
* @returns True if any subsequent argument matches source or exists in source array
*/
export function inop(...args: unknown[]): boolean {
if (args.length < 2) return false;
const [source, ...searchValues] = args;
// Handle array-like objects
if (source !== null && 'length' in (source as { length?: number })) {
const arr = Array.from(source as ArrayLike<unknown>);
return searchValues.some(val => arr.includes(val));
}
// Handle single value comparison
return searchValues.some(val => source === val);
}
/**
* Case-insensitive version of inop() for string comparisons
* @param args - First argument is source (string/array), followed by values to check
* @returns True if any subsequent argument matches source with case-insensitive comparison
*/
export function iinop(...args: unknown[]): boolean {
if (args.length < 2) return false;
const [source, ...searchValues] = args;
// Handle array-like objects
if (source !== null && 'length' in (source as { length?: number })) {
const arr = Array.from(source as ArrayLike<unknown>);
return searchValues.some(val => {
return arr.some(item => {
if (typeof item === 'string' && typeof val === 'string') {
return item.toLowerCase() === val.toLowerCase();
}
return item === val;
});
});
}
// Handle single value comparison
if (typeof source === 'string') {
return searchValues.some(val => {
if (typeof val === 'string') {
return source.toLowerCase() === val.toLowerCase();
}
return source === val;
});
}
return searchValues.some(val => source === val);
}
/**
* Base date for Clarion date calculations (December 28, 1800)
*/
const CLARION_EPOCH = new Date(1800, 11, 28);
/**
* Converts a Clarion integer time value to a formatted time string
* @param val - Clarion time value (HHMMSS.CC format where CC is centiseconds)
* @param detail - If true, includes centiseconds in output
* @returns Formatted time string (HH:MM:SS or HH:MM:SS.CC)
*/
export function clarionIntToTime(val: number, detail?: boolean): string {
// Ensure non-negative value
if (val < 0) {
val = 0;
}
// Convert to seconds
const sec_num = val / 100;
// Extract time components
const hours: number = Math.floor(val / 360000);
const minutes: number = Math.floor((sec_num - hours * 3600) / 60);
const seconds: number = Math.floor(
sec_num - hours * 3600 - minutes * 60
);
const ms: number = Math.floor(
val - hours * 360000 - minutes * 6000 - seconds * 100
);
// Format time components with leading zeros
const paddedHours = hours.toString().padStart(2, '0');
const paddedMinutes = minutes.toString().padStart(2, '0');
const paddedSeconds = seconds.toString().padStart(2, '0');
// Handle centiseconds formatting
let msString: string;
if (ms < 10) {
msString = ms.toString() + '00';
} else if (ms < 100) {
msString = ms.toString() + '0';
} else {
msString = ms.toString();
}
// Return formatted time string
return detail
? `${paddedHours}:${paddedMinutes}:${paddedSeconds}.${msString}`
: `${paddedHours}:${paddedMinutes}:${paddedSeconds}`;
}
/**
* Converts a time string to a Clarion integer time value
* @param timeStr - Time string in format "HH:MM:SS" or "HH:MM:SS.CC"
* @returns Clarion time value (HHMMSS.CC format where CC is centiseconds)
* @throws Error if the time string format is invalid
*/
export function clarionTimeToInt(timeStr: string): number {
// Regular expressions to match both formats
const basicTimeRegex = /^(\d{2}):(\d{2}):(\d{2})$/;
const detailedTimeRegex = /^(\d{2}):(\d{2}):(\d{2})\.(\d{1,3})$/;
let hours: number;
let minutes: number;
let seconds: number;
let centiseconds: number = 0;
// Try matching both formats
const basicMatch = timeStr.match(basicTimeRegex);
const detailedMatch = timeStr.match(detailedTimeRegex);
if (detailedMatch) {
// Parse detailed time format (HH:MM:SS.CC)
[, hours, minutes, seconds, centiseconds] = detailedMatch.map(Number);
// Handle different centisecond precision
if (centiseconds < 10) {
centiseconds *= 100;
} else if (centiseconds < 100) {
centiseconds *= 10;
}
} else if (basicMatch) {
// Parse basic time format (HH:MM:SS)
[, hours, minutes, seconds] = basicMatch.map(Number);
} else {
throw new Error('Invalid time format. Expected HH:MM:SS or HH:MM:SS.CC');
}
// Validate time components
if (hours >= 24 || minutes >= 60 || seconds >= 60 || centiseconds >= 1000) {
throw new Error('Invalid time values');
}
// Convert to Clarion integer format
return hours * 360000 + minutes * 6000 + seconds * 100 + centiseconds;
}
/**
* Gets the current Clarion clock value (centiseconds since midnight)
* @returns Number of centiseconds since midnight
*/
export function clarionClock(): number {
// Get current date and midnight
const today = new Date();
const midnight = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate(),
0,
0,
0,
0
);
// Calculate milliseconds since midnight
const millisecondsPassed = today.getTime() - midnight.getTime();
// Convert to centiseconds and add 1 (Clarion offset)
// Division by 10 converts milliseconds to centiseconds
return Math.floor(millisecondsPassed / 10 + 1);
}
/**
* Converts a JavaScript Date object to a Clarion date integer
* @param date - JavaScript Date object
* @returns Number of days since December 28, 1800 (Clarion date format)
*/
export function clarionDateToInt(date: Date): number {
// Clone the input date to avoid modifying it
const inputDate = new Date(date);
// Set time to noon to avoid daylight saving time issues
inputDate.setHours(12, 0, 0, 0);
CLARION_EPOCH.setHours(12, 0, 0, 0);
// Calculate days difference
const diffTime = inputDate.getTime() - CLARION_EPOCH.getTime();
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
}
/**
* Converts a Clarion date integer to a JavaScript Date object
* @param days - Number of days since December 28, 1800 (Clarion date format)
* @returns JavaScript Date object
*/
export function clarionIntToDate(days: number): Date {
// Create new date to avoid modifying the epoch constant
const resultDate = new Date(CLARION_EPOCH);
// Set to noon to avoid daylight saving time issues
resultDate.setHours(12, 0, 0, 0);
// Add the days
resultDate.setDate(CLARION_EPOCH.getDate() + days);
return resultDate;
}
/**
* Converts a date string to a Clarion date integer
* @param dateStr - Date string in format "YYYY-MM-DD" or "MM/DD/YYYY"
* @returns Number of days since December 28, 1800 (Clarion date format)
* @throws Error if the date string format is invalid
*/
export function clarionDateStringToInt(dateStr: string): number {
// Regular expressions for supported date formats
const isoFormatRegex = /^(\d{4})-(\d{2})-(\d{2})$/; // YYYY-MM-DD
const usFormatRegex = /^(\d{2})\/(\d{2})\/(\d{4})$/; // MM/DD/YYYY
let year: number;
let month: number;
let day: number;
const isoMatch = dateStr.match(isoFormatRegex);
const usMatch = dateStr.match(usFormatRegex);
if (isoMatch) {
[, year, month, day] = isoMatch.map(Number);
month--; // JavaScript months are 0-based
} else if (usMatch) {
[, month, day, year] = usMatch.map(Number);
month--; // JavaScript months are 0-based
} else {
throw new Error('Invalid date format. Expected YYYY-MM-DD or MM/DD/YYYY');
}
// Validate date components
if (month < 0 || month > 11 || day < 1 || day > 31 || year < 1800) {
throw new Error('Invalid date values');
}
const date = new Date(year, month, day);
// Check if the date is valid
if (date.getMonth() !== month || date.getDate() !== day) {
throw new Error('Invalid date');
}
return clarionDateToInt(date);
}
/**
* Converts a Clarion date integer to a formatted date string
* @param days - Number of days since December 28, 1800 (Clarion date format)
* @param format - Output format ('iso' for YYYY-MM-DD or 'us' for MM/DD/YYYY)
* @returns Formatted date string
*/
export function clarionIntToDateString(days: number, format: 'iso' | 'us' = 'iso'): string {
const date = clarionIntToDate(days);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return format === 'iso'
? `${year}-${month}-${day}`
: `${month}/${day}/${year}`;
}

175
src/strings/locale.ts Normal file
View File

@ -0,0 +1,175 @@
/**
* Comprehensive localization utilities for handling text, numbers, dates, and currencies
*/
// type LocaleConfig = {
// dateFormat: string;
// numberFormat: {
// decimal: string;
// thousands: string;
// precision: number;
// };
// currency: {
// symbol: string;
// position: 'prefix' | 'suffix';
// };
// pluralRules?: Intl.PluralRules;
// };
// const defaultConfig: LocaleConfig = {
// dateFormat: 'YYYY-MM-DD',
// numberFormat: {
// decimal: '.',
// thousands: ',',
// precision: 2
// },
// currency: {
// symbol: '$',
// position: 'prefix'
// }
// };
/**
* Formats a number according to locale settings
*/
export const formatNumber = (
value: number,
locale: string,
options?: Intl.NumberFormatOptions
): string => {
return new Intl.NumberFormat(locale, options).format(value);
};
/**
* Formats currency according to locale settings
*/
export const formatCurrency = (
value: number,
locale: string,
currency: string
): string => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(value);
};
/**
* Formats a date according to locale settings
*/
export const formatDate = (
date: Date,
locale: string,
options?: Intl.DateTimeFormatOptions
): string => {
return new Intl.DateTimeFormat(locale, options).format(date);
};
/**
* Formats relative time (e.g., "2 days ago")
*/
export const formatRelativeTime = (
value: number,
unit: Intl.RelativeTimeFormatUnit,
locale: string
): string => {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
return rtf.format(value, unit);
};
/**
* Handles plural forms based on locale rules
*/
export const getPlural = (
count: number,
locale: string,
forms: { [key: string]: string }
): string => {
const pluralRules = new Intl.PluralRules(locale);
const rule = pluralRules.select(count);
return forms[rule] || forms['other'];
};
/**
* Formats lists according to locale conventions
*/
export const formatList = (
items: string[],
locale: string,
type: 'conjunction' | 'disjunction' = 'conjunction'
): string => {
return new Intl.ListFormat(locale, { type }).format(items);
};
/**
* Compares strings according to locale rules
*/
export const compareStrings = (
str1: string,
str2: string,
locale: string
): number => {
return new Intl.Collator(locale).compare(str1, str2);
};
/**
* Formats percentages according to locale
*/
export const formatPercent = (
value: number,
locale: string,
decimals: number = 0
): string => {
return new Intl.NumberFormat(locale, {
style: 'percent',
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(value);
};
/**
* Formats units according to locale
*/
export const formatUnit = (
value: number,
unit: string,
locale: string
): string => {
return new Intl.NumberFormat(locale, {
style: 'unit',
unit: unit
}).format(value);
};
/**
* Converts number words to digits based on locale
*/
export const parseNumberWords = (
text: string,
locale: string
): number | null => {
const numberWords: { [key: string]: number } = {
zero: 0, one: 1, two: 2, three: 3, four: 4,
five: 5, six: 6, seven: 7, eight: 8, nine: 9
};
const localizedWords = Object.keys(numberWords).map(word =>
new Intl.NumberFormat(locale).format(numberWords[word])
);
const normalized = text.toLowerCase();
for (let i = 0; i < localizedWords.length; i++) {
if (normalized.includes(localizedWords[i].toLowerCase())) {
return i;
}
}
return null;
};
/**
* Handles bi-directional text
*/
export const handleBiDi = (text: string): string => {
// Add Unicode control characters for bi-directional text
return `\u202A${text}\u202C`;
};

72
src/strings/replace.ts Normal file
View File

@ -0,0 +1,72 @@
// biome-rule-off
// biome-disable lint/style/noInferrableTypes
/**
* Replaces a specific occurrence of a substring in a string
* @param {string} str - The input string
* @param {string} search - The string to search for
* @param {string} replacement - The string to replace with
* @param {number} [occurrence=1] - Which occurrence to replace (1-based). Defaults to first occurrence
* @returns {string} The string with the specified occurrence replaced
*/
export function replaceStr(
str: string,
search: string,
replacement: string,
// biome-ignore lint: Stupid biome Rule
occurrence: number = 1
): string {
if (!str || !search || occurrence < 1) return str
let currentIndex = 0
let currentOccurrence = 0
while (currentIndex < str.length) {
const index = str.indexOf(search, currentIndex)
if (index === -1) break
currentOccurrence++
if (currentOccurrence === occurrence) {
return (
str.slice(0, index) + replacement + str.slice(index + search.length)
)
}
currentIndex = index + 1
}
return str
}
/**
* Replaces multiple occurrences of a substring in a string
* @param {string} str - The input string
* @param {string} search - The string to search for
* @param {string} replacement - The string to replace with
* @param {number} [times=Infinity] - Maximum number of replacements to make. Defaults to all occurrences
* @returns {string} The string with the specified number of occurrences replaced
*/
export function replaceStrAll(
str: string,
search: string,
replacement: string,
times: number = Number.POSITIVE_INFINITY
): string {
if (!str || !search || times < 1) return str
let result = str
let currentIndex = 0
let count = 0
while (currentIndex < result.length && count < times) {
const index = result.indexOf(search, currentIndex)
if (index === -1) break
result =
result.slice(0, index) + replacement + result.slice(index + search.length)
currentIndex = index + replacement.length
count++
}
return result
}

53
src/strings/trim.ts Normal file
View File

@ -0,0 +1,53 @@
/**
* Trims specified characters from the left side of a string a specified number of times.
* @param {string} str - The input string to trim
* @param {string} [chars=' '] - The characters to trim (if multiple, any of them will be trimmed)
* @param {number} [times=Infinity] - Number of times to trim. Defaults to all occurrences
* @returns {string} The trimmed string
*/
export function trimLeft(
str: string,
// biome-ignore lint: Stupid biome Rule
chars: string = ' ',
times: number = Number.POSITIVE_INFINITY
): string {
if (!str) return str
let count = 0
let startIdx = 0
const charSet = new Set(chars)
while (startIdx < str.length && charSet.has(str[startIdx]) && count < times) {
startIdx++
count++
}
return str.slice(startIdx)
}
/**
* Trims specified characters from the right side of a string a specified number of times.
* @param {string} str - The input string to trim
* @param {string} [chars=' '] - The characters to trim (if multiple, any of them will be trimmed)
* @param {number} [times=Infinity] - Number of times to trim. Defaults to all occurrences
* @returns {string} The trimmed string
*/
export function trimRight(
str: string,
// biome-ignore lint: Stupid biome Rule
chars: string = ' ',
times: number = Number.POSITIVE_INFINITY
): string {
if (!str) return str
let count = 0
let endIdx = str.length - 1
const charSet = new Set(chars)
while (endIdx >= 0 && charSet.has(str[endIdx]) && count < times) {
endIdx--
count++
}
return str.slice(0, endIdx + 1)
}

10
tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"declaration": true,
"target": "ES6",
"declarationDir": "./dist",
"moduleResolution": "node",
"lib": ["ES6", "DOM","ESNext"],
"emitDeclarationOnly": false
}
}

34
vite.config.js Normal file
View File

@ -0,0 +1,34 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import dts from 'vite-plugin-dts'; // You'll need to install this
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
formats: ['es'],
fileName: 'index'
},
rollupOptions: {
external: [
'fs',
'path',
'url',
'chalk',
'semver',
'yargs',
'yargs/helpers'
]
},
target: 'node16',
outDir: 'dist',
emptyOutDir: true,
sourcemap: true
}, plugins: [
dts({
insertTypesEntry: true,
outDir: 'dist'
})
]
});