diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..81229e8d Binary files /dev/null and b/.DS_Store differ diff --git a/.cursor/rules/nodejs-api-service.mdc b/.cursor/rules/nodejs-api-service.mdc new file mode 100644 index 00000000..8ec8e991 --- /dev/null +++ b/.cursor/rules/nodejs-api-service.mdc @@ -0,0 +1,12 @@ +--- +description: Node.js API service +globs: +alwaysApply: true +--- + +- Use JSDoc standard for creating docblocks of functions and classes. +- Always use camelCase for function names. +- Always use upper-case snake_case for constants. +- Create integration tests in 'tests/integration' that use node-assert, which run with mocha. +- Create unit tests in 'tests/unit' that use node-assert, which run with mocha. +- Use node.js community "Best Practices". diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3c5be725..02ae4b77 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,4 +9,4 @@ updates: directory: "/" schedule: interval: daily - open-pull-requests-limit: 10 \ No newline at end of file + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0a6d82e..b62ace73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x] + node-version: [20.x, 22.x, 24.x] steps: - name: Checkout Repository @@ -30,7 +30,7 @@ jobs: run: npm install - name: Build - run: npm run rollup + run: npm run build - name: Run Tests run: npm run test @@ -44,4 +44,4 @@ jobs: steps: - uses: fastify/github-action-merge-dependabot@v3 with: - github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index cbce65cd..f0f37448 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -/node_modules/ -/test/webpack/ +node_modules .idea -.nyc_output -*.tgz \ No newline at end of file +coverage +*.tgz diff --git a/README.md b/README.md index 86482ee7..8400f0de 100644 --- a/README.md +++ b/README.md @@ -1,685 +1,1330 @@ # Haro -[![npm version](https://img.shields.io/npm/v/haro.svg)](https://www.npmjs.com/package/haro) -[![License: BSD-3](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](./LICENSE) -[![Build Status](https://img.shields.io/github/actions/workflow/status/avoidwork/haro/ci.yml?branch=main)](https://github.com/avoidwork/haro/actions) - -**A simple, fast, and flexible way to organize and search your data.** - ---- - -Need a simple way to keep track of information—like contacts, lists, or notes? Haro helps you organize, find, and update your data quickly, whether you’re using it in a website, an app, or just on your computer. It’s like having a super-organized digital assistant for your information. - -## Table of Contents -- [Features](#key-features) -- [Installation](#installation) -- [Usage](#usage) -- [Examples](#examples) -- [API](#api) -- [Configuration](#configuration) -- [Contributing](#contributing) -- [License](#license) -- [Changelog](#changelog) -- [Support](#support) - -## Key Features -- **Easy to use**: Works out of the box, no complicated setup. -- **Very fast**: Quickly finds and updates your information. -- **Keeps a history**: Remembers changes, so you can see what something looked like before. -- **Flexible**: Use it for any type of data—contacts, tasks, notes, and more. -- **Works anywhere**: Use it in your website, app, or server. - -## How Does It Work? -Imagine you have a box of index cards, each with information about a person or thing. Haro helps you sort, search, and update those cards instantly. If you make a change, Haro remembers the old version too. You can ask Haro questions like “Who is named Jane?” or “Show me everyone under 30.” - -## Who Is This For? -- Anyone who needs to keep track of information in an organized way. -- People building websites or apps who want an easy way to manage data. -- Developers looking for a fast, reliable data storage solution. +[![npm version](https://badge.fury.io/js/haro.svg)](https://badge.fury.io/js/haro) +[![Node.js Version](https://img.shields.io/node/v/haro.svg)](https://nodejs.org/) +[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) +[![Build Status](https://github.com/avoidwork/haro/actions/workflows/ci.yml/badge.svg)](https://github.com/avoidwork/haro/actions) + +A fast, flexible immutable DataStore for collections of records with indexing, versioning, and advanced querying capabilities. Provides a Map-like interface with powerful search and filtering features. ## Installation -Install with npm: +### npm ```sh npm install haro ``` -Or with yarn: +### yarn ```sh yarn add haro ``` +### pnpm + +```sh +pnpm add haro +``` + ## Usage -Haro is available as both an ES module and CommonJS module. +### Factory Function -### Import (ESM) ```javascript import { haro } from 'haro'; +const store = haro(data, config); ``` -### Require (CommonJS) +### Class Constructor + ```javascript -const { haro } = require('haro'); +import { Haro } from 'haro'; + +// Create a store with indexes and versioning +const store = new Haro({ + index: ['name', 'email', 'department'], + key: 'id', + versioning: true, + immutable: true +}); + +// Create store with initial data +const users = new Haro([ + { name: 'Alice', email: 'alice@company.com', department: 'Engineering' }, + { name: 'Bob', email: 'bob@company.com', department: 'Sales' } +], { + index: ['name', 'department'], + versioning: true +}); ``` -### Creating a Store -Haro takes two optional arguments: an array of records to set asynchronously, and a configuration object. +### Class Inheritance ```javascript -const storeDefaults = haro(); -const storeRecords = haro([ - { name: 'Alice', age: 30 }, - { name: 'Bob', age: 28 } -]); -const storeCustom = haro(null, { key: 'id' }); +import { Haro } from 'haro'; + +class UserStore extends Haro { + constructor(config) { + super({ + index: ['email', 'department', 'role'], + key: 'id', + versioning: true, + ...config + }); + } + + beforeSet(key, data, batch, override) { + // Validate email format + if (data.email && !this.isValidEmail(data.email)) { + throw new Error('Invalid email format'); + } + } + + onset(record, batch) { + console.log(`User ${record.name} was added/updated`); + } + + isValidEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + } +} ``` -## Examples +## Parameters + +### delimiter +**String** - Delimiter for composite indexes (default: `'|'`) -### Example 1: Manage a Contact List ```javascript -import { haro } from 'haro'; +const store = haro(null, { delimiter: '::' }); +``` -// Create a store with indexes for name and email -const contacts = haro(null, { index: ['name', 'email'] }); +### id +**String** - Unique identifier for this store instance. Auto-generated if not provided. -// Add realistic contacts -contacts.batch([ - { name: 'Alice Johnson', email: 'alice.j@example.com', company: 'Acme Corp', phone: '555-1234' }, - { name: 'Carlos Rivera', email: 'carlos.r@example.com', company: 'Rivera Designs', phone: '555-5678' }, - { name: 'Priya Patel', email: 'priya.p@example.com', company: 'InnovateX', phone: '555-8765' } -], 'set'); +```javascript +const store = haro(null, { id: 'user-cache' }); +``` + +### immutable +**Boolean** - Return frozen/immutable objects for data safety (default: `false`) -// Find a contact by email -console.log(contacts.find({ email: 'carlos.r@example.com' })); -// → [[$uuid, { name: 'Carlos Rivera', email: 'carlos.r@example.com', company: 'Rivera Designs', phone: '555-5678' }]] +```javascript +const store = haro(null, { immutable: true }); +``` -// Search contacts by company -console.log(contacts.search(/^acme/i, 'company')); -// → [[$uuid, { name: 'Alice Johnson', email: 'alice.j@example.com', company: 'Acme Corp', phone: '555-1234' }]] +### index +**Array** - Fields to index for faster searches. Supports composite indexes using delimiter. -// Search contacts with phone numbers ending in '78' -console.log(contacts.search(phone => phone.endsWith('78'), 'phone')); -// → [[$uuid, { name: 'Carlos Rivera', ... }]] +```javascript +const store = haro(null, { + index: ['name', 'email', 'name|department', 'department|role'] +}); ``` -### Example 2: Track Project Tasks +### key +**String** - Primary key field name (default: `'id'`) + ```javascript -import { haro } from 'haro'; +const store = haro(null, { key: 'userId' }); +``` -// Create a store for project tasks, indexed by status and assignee -const tasks = haro(null, { index: ['status', 'assignee'] }); +### versioning +**Boolean** - Enable MVCC-style versioning to track record changes (default: `false`) -tasks.batch([ - { title: 'Design homepage', status: 'in progress', assignee: 'Alice', due: '2025-05-20' }, - { title: 'Fix login bug', status: 'open', assignee: 'Carlos', due: '2025-05-18' }, - { title: 'Deploy to production', status: 'done', assignee: 'Priya', due: '2025-05-15' } -], 'set'); +```javascript +const store = haro(null, { versioning: true }); +``` + +### Parameter Validation + +The constructor validates configuration and provides helpful error messages: -// Find all open tasks -console.log(tasks.find({ status: 'open' })); -// → [[$uuid, { title: 'Fix login bug', status: 'open', assignee: 'Carlos', due: '2025-05-18' }]] +```javascript +// Invalid index configuration will provide clear feedback +try { + const store = new Haro({ index: 'name' }); // Should be array +} catch (error) { + console.error(error.message); // Clear validation error +} -// Search tasks assigned to Alice -console.log(tasks.search('Alice', 'assignee')); -// → [[$uuid, { title: 'Design homepage', ... }]] +// Missing required configuration +try { + const store = haro([{id: 1}], { key: 'nonexistent' }); +} catch (error) { + console.error('Key field validation error'); +} ``` -### Example 3: Track Order Status Changes (Versioning) +## Interoperability + +### Array Methods Compatibility + +Haro provides Array-like methods for familiar data manipulation: + ```javascript import { haro } from 'haro'; -// Enable versioning for order tracking -const orders = haro(null, { versioning: true }); +const store = haro([ + { id: 1, name: 'Alice', age: 30 }, + { id: 2, name: 'Bob', age: 25 }, + { id: 3, name: 'Charlie', age: 35 } +]); + +// Use familiar Array methods +const adults = store.filter(record => record.age >= 30); +const names = store.map(record => record.name); +const totalAge = store.reduce((sum, record) => sum + record.age, 0); + +store.forEach((record, key) => { + console.log(`${key}: ${record.name} (${record.age})`); +}); +``` -// Add a new order and update its status -let rec = orders.set(null, { id: 1001, customer: 'Priya Patel', status: 'processing' }); -rec = orders.set(rec[0], { id: 1001, customer: 'Priya Patel', status: 'shipped' }); -rec = orders.set(rec[0], { id: 1001, customer: 'Priya Patel', status: 'delivered' }); +### Event-Driven Architecture -// See all status changes for the order -orders.versions.get(rec[0]).forEach(([data]) => console.log(data)); -// Output: -// { id: 1001, customer: 'Priya Patel', status: 'processing' } -// { id: 1001, customer: 'Priya Patel', status: 'shipped' } -// { id: 1001, customer: 'Priya Patel', status: 'delivered' } +Compatible with event-driven patterns through lifecycle hooks: -// { note: 'Initial' } -// { note: 'Updated' } +```javascript +class EventedStore extends Haro { + constructor(eventEmitter, config) { + super(config); + this.events = eventEmitter; + } + + onset(record, batch) { + this.events.emit('record:created', record); + } + + ondelete(key, batch) { + this.events.emit('record:deleted', key); + } +} ``` -These examples show how Haro can help you manage contacts, tasks, and keep a history of changes with just a few lines of code. +## Testing -## Configuration -### beforeBatch -_Function_ +Haro maintains comprehensive test coverage across all features with **148 passing tests**: -Event listener for before a batch operation, receives `type`, `data`. +``` +--------------|---------|----------|---------|---------|------------------------- +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s +--------------|---------|----------|---------|---------|------------------------- +All files | 100 | 96.95 | 100 | 100 | + constants.js | 100 | 100 | 100 | 100 | + haro.js | 100 | 96.94 | 100 | 100 | 205-208,667,678,972-976 +--------------|---------|----------|---------|---------|------------------------- +``` -### beforeClear -_Function_ +### Test Organization -Event listener for before clearing the data store. +The test suite is organized into focused areas: -### beforeDelete -_Function_ +- **Basic CRUD Operations** - Core data manipulation (set, get, delete, clear) +- **Indexing** - Index creation, composite indexes, and reindexing +- **Searching & Filtering** - find(), where(), search(), filter(), and sortBy() methods +- **Immutable Mode** - Data freezing and immutability guarantees +- **Versioning** - MVCC-style record versioning +- **Lifecycle Hooks** - beforeSet, onset, ondelete, etc. +- **Utility Methods** - clone(), merge(), limit(), map(), reduce(), etc. +- **Error Handling** - Validation and error scenarios +- **Factory Function** - haro() factory with various initialization patterns -Event listener for before a record is deleted, receives `key`, `batch`. +### Running Tests -### beforeSet -_Function_ +```bash +# Run unit tests +npm test -Event listener for before a record is set, receives `key`, `data`. +# Run with coverage +npm run test:coverage -### index -_Array_ +# Run integration tests +npm run test:integration -Array of values to index. Composite indexes are supported, by using the default delimiter (`this.delimiter`). -Non-matches within composites result in blank values. +# Run performance benchmarks +npm run benchmark +``` -Example of fields/properties to index: -```javascript -const store = haro(null, {index: ['field1', 'field2', 'field1|field2|field3']}); +## Benchmarks + +Haro includes comprehensive benchmark suites for performance analysis and comparison with other data store solutions. + +### Latest Performance Results + +**Overall Performance Summary:** +- **Total Tests**: 572 tests across 9 categories +- **Total Runtime**: 1.6 minutes +- **Best Performance**: HAS operation (20,815,120 ops/second on 1,000 records) +- **Memory Efficiency**: Highly efficient with minimal overhead for typical workloads + +### Benchmark Categories + +#### Basic Operations +- **SET operations**: Record creation, updates, overwrites +- **GET operations**: Single record retrieval, cache hits/misses +- **DELETE operations**: Record removal and index cleanup +- **BATCH operations**: Bulk insert/update/delete performance + +**Performance Highlights:** +- SET operations: Up to 3.2M ops/sec for typical workloads +- GET operations: Up to 20M ops/sec with index lookups +- DELETE operations: Efficient cleanup with index maintenance +- BATCH operations: Optimized for bulk data manipulation + +#### Search & Query Operations +- **INDEX queries**: Using find() with indexed fields +- **FILTER operations**: Predicate-based filtering +- **SEARCH operations**: Text and regex searching +- **WHERE clauses**: Complex query conditions + +**Performance Highlights:** +- Indexed FIND queries: Up to 64,594 ops/sec (1,000 records) +- FILTER operations: Up to 46,255 ops/sec +- Complex queries: Maintains good performance with multiple conditions +- Memory-efficient query processing + +#### Advanced Features +- **VERSION tracking**: Performance impact of versioning +- **IMMUTABLE mode**: Object freezing overhead +- **COMPOSITE indexes**: Multi-field index performance +- **Memory usage**: Efficient memory consumption patterns +- **Utility operations**: clone, merge, freeze, forEach performance +- **Pagination**: Limit-based result pagination +- **Persistence**: Data dump/restore operations + +### Running Benchmarks + +```bash +# Run all benchmarks +node benchmarks/index.js + +# Run specific benchmark categories +node benchmarks/index.js --basic-only # Basic CRUD operations +node benchmarks/index.js --search-only # Search and query operations +node benchmarks/index.js --index-only # Index operations +node benchmarks/index.js --memory-only # Memory usage analysis +node benchmarks/index.js --comparison-only # vs native structures +node benchmarks/index.js --utilities-only # Utility operations +node benchmarks/index.js --pagination-only # Pagination performance +node benchmarks/index.js --persistence-only # Persistence operations +node benchmarks/index.js --immutable-only # Immutable vs mutable + +# Run with memory analysis +node --expose-gc benchmarks/memory-usage.js ``` -### key -_String_ +### Performance Comparison with Native Structures -Optional `Object` key to utilize as `Map` key, defaults to a version 4 `UUID` if not specified, or found. +**Storage Operations:** +- Haro vs Map: Comparable performance for basic operations +- Haro vs Array: Slower for simple operations, faster for complex queries +- Haro vs Object: Trade-off between features and raw performance -Example of specifying the primary key: -```javascript -const store = haro(null, {key: 'field'}); -``` +**Query Operations:** +- Haro FIND (indexed): 64,594 ops/sec vs Array filter: 189,293 ops/sec +- Haro provides advanced query capabilities not available in native structures +- Memory overhead justified by feature richness -### logging -_Boolean_ +### Memory Efficiency -Logs persistent storage messages to `console`, default is `true`. +**Memory Usage Comparison (50,000 records):** +- Haro: 13.98 MB +- Map: 3.52 MB +- Object: 1.27 MB +- Array: 0.38 MB -### onbatch -_Function_ +**Memory Analysis:** +- Reasonable overhead for feature set provided +- Efficient index storage and maintenance +- Garbage collection friendly -Event listener for a batch operation, receives two arguments ['type', `Array`]. +### Performance Tips -### onclear -_Function_ +For optimal performance: -Event listener for clearing the data store. +1. **Use indexes wisely** - Index fields you'll query frequently +2. **Choose appropriate key strategy** - Shorter keys perform better +3. **Batch operations** - Use batch() for multiple changes +4. **Consider immutable mode cost** - Only enable if needed for data safety +5. **Minimize version history** - Disable versioning if not required +6. **Use pagination** - Implement limit() for large result sets +7. **Leverage utility methods** - Use built-in clone, merge, freeze for safety -### ondelete -_Function_ +### Performance Indicators -Event listener for when a record is deleted, receives the record key. +* ✅ **Indexed queries** significantly outperform filters (64k vs 46k ops/sec) +* ✅ **Batch operations** provide excellent bulk performance +* ✅ **Get operations** consistently outperform set operations +* ✅ **Memory usage** remains stable under load +* ✅ **Utility operations** perform well (clone: 1.6M ops/sec) -### onoverride -_Function_ +### Immutable vs Mutable Mode -Event listener for when the data store changes entire data set, receives a `String` naming what changed (`indexes` or `records`). +**Performance Impact:** +- Creation: Minimal difference (1.27x faster mutable) +- Read operations: Comparable performance +- Write operations: Slight advantage to mutable mode +- Transformation operations: Significant performance cost in immutable mode -### onset -_Function_ +**Recommendations:** +- Use immutable mode for data safety in multi-consumer environments +- Use mutable mode for high-frequency write operations +- Consider the trade-off between safety and performance -Event listener for when a record is set, receives an `Array`. +See `benchmarks/README.md` for complete documentation and advanced usage. -### versioning -_Boolean_ +## API Reference -Enable/disable MVCC style versioning of records, default is `false`. Versions are stored in `Sets` for easy iteration. +### Properties + +#### data +`{Map}` - Internal Map of records, indexed by key -Example of enabling versioning: ```javascript -const store = haro(null, {versioning: true}); +const store = haro(); +console.log(store.data.size); // 0 ``` -## Properties -### data -_Map_ +#### delimiter +`{String}` - The delimiter used for composite indexes -`Map` of records, updated by `del()` & `set()`. - -### indexes -_Map_ +```javascript +const store = haro(null, { delimiter: '|' }); +console.log(store.delimiter); // '|' +``` -Map of indexes, which are Sets containing Map keys. +#### id +`{String}` - Unique identifier for this store instance -### registry -_Array_ +```javascript +const store = haro(null, { id: 'my-store' }); +console.log(store.id); // 'my-store' +``` -Array representing the order of `this.data`. +#### immutable +`{Boolean}` - Whether the store returns immutable objects -### size -_Number_ +```javascript +const store = haro(null, { immutable: true }); +console.log(store.immutable); // true +``` -Number of records in the DataStore. +#### index +`{Array}` - Array of indexed field names -### versions -_Map_ +```javascript +const store = haro(null, { index: ['name', 'email'] }); +console.log(store.index); // ['name', 'email'] +``` -`Map` of `Sets` of records, updated by `set()`. +#### indexes +`{Map}` - Map of indexes containing Sets of record keys -## API -### batch(array, type) -_Array_ +```javascript +const store = haro(); +console.log(store.indexes); // Map(0) {} +``` -The first argument must be an `Array`, and the second argument must be `del` or `set`. +#### key +`{String}` - The primary key field name ```javascript -const haro = require('haro'), - store = haro(null, {key: 'id', index: ['name']}), - nth = 100, - data = []; - -let i = -1; +const store = haro(null, { key: 'userId' }); +console.log(store.key); // 'userId' +``` -while (++i < nth) { - data.push({id: i, name: 'John Doe' + i}); -} +#### registry +`{Array}` - Array of all record keys (read-only property) -// records is an Array of Arrays -const records = store.batch(data, 'set'); +```javascript +const store = haro(); +store.set('key1', { name: 'Alice' }); +console.log(store.registry); // ['key1'] ``` -### clear() -_self_ +#### size +`{Number}` - Number of records in the store (read-only property) -Removes all key/value pairs from the DataStore. - -Example of clearing a DataStore: ```javascript const store = haro(); +console.log(store.size); // 0 +``` -// Data is added +#### versions +`{Map}` - Map of version history (when versioning is enabled) -store.clear(); +```javascript +const store = haro(null, { versioning: true }); +console.log(store.versions); // Map(0) {} ``` -### del(key) -_Undefined_ +#### versioning +`{Boolean}` - Whether versioning is enabled -Deletes the record. +```javascript +const store = haro(null, { versioning: true }); +console.log(store.versioning); // true +``` + +### Methods + +#### batch(array, type) + +Performs batch operations on multiple records for efficient bulk processing. + +**Parameters:** +- `array` `{Array}` - Array of records to process +- `type` `{String}` - Operation type: `'set'` or `'del'` (default: `'set'`) + +**Returns:** `{Array}` Array of results from the batch operation -Example of deleting a record: ```javascript -const store = haro(), - rec = store.set(null, {abc: true}); +const results = store.batch([ + { name: 'Alice', age: 30 }, + { name: 'Bob', age: 28 } +], 'set'); -store.del(rec[0]); -console.log(store.size); // 0 +// Delete multiple records +store.batch(['key1', 'key2'], 'del'); ``` -### dump(type="records") -_Array_ or _Object_ +**See also:** set(), delete() + +#### clear() + +Removes all records, indexes, and versions from the store. -Returns the records or indexes of the DataStore as mutable `Array` or `Object`, for the intention of reuse/persistent storage without relying on an adapter which would break up the data set. +**Returns:** `{Haro}` Store instance for chaining ```javascript -const store = haro(); +store.clear(); +console.log(store.size); // 0 +``` -// Data is loaded +**See also:** delete() -const records = store.dump(); -const indexes = store.dump('indexes'); +#### clone(arg) -// Save records & indexes -``` +Creates a deep clone of the given value, handling objects, arrays, and primitives. -### entries() -_MapIterator_ +**Parameters:** +- `arg` `{*}` - Value to clone (any type) -Returns a new `Iterator` object that contains an array of `[key, value]` for each element in the `Map` object in -insertion order. +**Returns:** `{*}` Deep clone of the argument -Example of deleting a record: ```javascript -const store = haro(); -let item, iterator; +const original = { name: 'John', tags: ['user', 'admin'] }; +const cloned = store.clone(original); +cloned.tags.push('new'); // original.tags is unchanged +``` + +#### delete(key, batch) -// Data is added +Deletes a record from the store and removes it from all indexes. -iterator = store.entries(); -item = iterator.next(); +**Parameters:** +- `key` `{String}` - Key of record to delete +- `batch` `{Boolean}` - Whether this is part of a batch operation (default: `false`) -do { - console.log(item.value); - item = iterator.next(); -} while (!item.done); +**Returns:** `{undefined}` + +**Throws:** `{Error}` If record with the specified key is not found + +```javascript +store.delete('user123'); ``` -### filter(callbackFn[, raw=false]) -_Array_ +**See also:** has(), clear(), batch() + +#### dump(type) -Returns an `Array` of double `Arrays` with the shape `[key, value]` for records which returned `true` to -`callbackFn(value, key)`. +Exports complete store data or indexes for persistence or debugging. + +**Parameters:** +- `type` `{String}` - Type of data to export: `'records'` or `'indexes'` (default: `'records'`) + +**Returns:** `{Array}` Array of [key, value] pairs or serialized index structure -Example of filtering a DataStore: ```javascript -const store = haro(); +const records = store.dump('records'); +const indexes = store.dump('indexes'); +// Use for persistence or backup +fs.writeFileSync('backup.json', JSON.stringify(records)); +``` + +**See also:** override() + +#### each(array, fn) + +Utility method to iterate over an array with a callback function. -// Data is added +**Parameters:** +- `array` `{Array}` - Array to iterate over +- `fn` `{Function}` - Function to call for each element -store.filter(function (value) { - return value.something === true; +**Returns:** `{Array}` The original array for method chaining + +```javascript +store.each([1, 2, 3], (item, index) => { + console.log(`Item ${index}: ${item}`); }); ``` -### find(where[, raw=false]) -_Array_ +#### entries() -Returns an `Array` of double `Arrays` with found by indexed values matching the `where`. +Returns an iterator of [key, value] pairs for each record in the store. + +**Returns:** `{Iterator}` Iterator of [key, value] pairs -Example of finding a record(s) with an identity match: ```javascript -const store = haro(null, {index: ['field1']}); +for (const [key, value] of store.entries()) { + console.log(`${key}:`, value); +} +``` + +**See also:** keys(), values() + +#### filter(fn, raw) + +Filters records using a predicate function, similar to Array.filter. -// Data is added +**Parameters:** +- `fn` `{Function}` - Predicate function to test each record +- `raw` `{Boolean}` - Whether to return raw data (default: `false`) -store.find({field1: 'some value'}); +**Returns:** `{Array}` Array of records that pass the predicate test + +**Throws:** `{Error}` If fn is not a function + +```javascript +const adults = store.filter(record => record.age >= 18); +const recentUsers = store.filter(record => + record.created > Date.now() - 86400000 +); ``` -### forEach(callbackFn[, thisArg]) -_Undefined_ +**See also:** find(), where(), map() + +#### find(where, raw) -Calls `callbackFn` once for each key-value pair present in the `Map` object, in insertion order. If a `thisArg` -parameter is provided to `forEach`, it will be used as the `this` value for each callback. +Finds records matching the specified criteria using indexes for optimal performance. + +**Parameters:** +- `where` `{Object}` - Object with field-value pairs to match +- `raw` `{Boolean}` - Whether to return raw data (default: `false`) + +**Returns:** `{Array}` Array of matching records -Example of deleting a record: ```javascript -const store = haro(); +const engineers = store.find({ department: 'Engineering' }); +const activeUsers = store.find({ status: 'active', role: 'user' }); +``` -store.set(null, {abc: true}); -store.forEach(function (value, key) { - console.log(key); +**See also:** where(), search(), filter() + +#### forEach(fn, ctx) + +Executes a function for each record in the store, similar to Array.forEach. + +**Parameters:** +- `fn` `{Function}` - Function to execute for each record +- `ctx` `{*}` - Context object to use as 'this' (default: store instance) + +**Returns:** `{Haro}` Store instance for chaining + +```javascript +store.forEach((record, key) => { + console.log(`${key}: ${record.name}`); }); ``` -### get(key[, raw=false]) -_Array_ +**See also:** map(), filter() -Gets the record as a double `Array` with the shape `[key, value]`. +#### freeze(...args) -Example of getting a record with a known primary key value: -```javascript -const store = haro(); +Creates a frozen array from the given arguments for immutable data handling. + +**Parameters:** +- `...args` `{*}` - Arguments to freeze into an array -// Data is added +**Returns:** `{Array}` Frozen array containing frozen arguments -store.get('keyValue'); +```javascript +const frozen = store.freeze(obj1, obj2, obj3); +// Returns Object.freeze([Object.freeze(obj1), ...]) ``` -### has(key) -_Boolean_ +#### get(key, raw) -Returns a `Boolean` indicating if the data store contains `key`. +Retrieves a record by its key. -Example of checking for a record with a known primary key value: -```javascript -const store = haro(); +**Parameters:** +- `key` `{String}` - Key of record to retrieve +- `raw` `{Boolean}` - Whether to return raw data (default: `false`) -// Data is added +**Returns:** `{Object|null}` The record if found, null if not found -store.has('keyValue'); // true or false +```javascript +const user = store.get('user123'); +const rawUser = store.get('user123', true); ``` -### keys() -_MapIterator_ +**See also:** has(), set() -Returns a new `Iterator` object that contains the keys for each element in the `Map` object in insertion order.` +#### has(key) -Example of getting an iterator, and logging the results: -```javascript -const store = haro(); -let item, iterator; +Checks if a record with the specified key exists in the store. -// Data is added +**Parameters:** +- `key` `{String}` - Key to check for existence -iterator = store.keys(); -item = iterator.next(); +**Returns:** `{Boolean}` True if record exists, false otherwise -do { - console.log(item.value); - item = iterator.next(); -} while (!item.done); +```javascript +if (store.has('user123')) { + console.log('User exists'); +} ``` -### limit(offset=0, max=0, raw=false) -_Array_ +**See also:** get(), delete() -Returns an `Array` of double `Arrays` with the shape `[key, value]` for the corresponding range of records. +#### keys() + +Returns an iterator of all keys in the store. + +**Returns:** `{Iterator}` Iterator of record keys -Example of paginating a data set: ```javascript -const store = haro(); +for (const key of store.keys()) { + console.log('Key:', key); +} +``` + +**See also:** values(), entries() -let ds1, ds2; +#### limit(offset, max, raw) -// Data is added +Returns a limited subset of records with offset support for pagination. -console.log(store.size); // >10 -ds1 = store.limit(0, 10); // [0-9] -ds2 = store.limit(10, 10); // [10-19] +**Parameters:** +- `offset` `{Number}` - Number of records to skip (default: `0`) +- `max` `{Number}` - Maximum number of records to return (default: `0`) +- `raw` `{Boolean}` - Whether to return raw data (default: `false`) -console.log(ds1.length === ds2.length); // true -console.log(JSON.stringify(ds1[0][1]) === JSON.stringify(ds2[0][1])); // false +**Returns:** `{Array}` Array of records within the specified range + +```javascript +const page1 = store.limit(0, 10); // First 10 records +const page2 = store.limit(10, 10); // Next 10 records +const page3 = store.limit(20, 10); // Records 21-30 ``` -### map(callbackFn, raw=false) -_Array_ +**See also:** toArray(), sort() -Returns an `Array` of the returns of `callbackFn(value, key)`. If `raw` is `true` an `Array` is returned. +#### map(fn, raw) -Example of mapping a DataStore: -```javascript -const store = haro(); +Transforms all records using a mapping function, similar to Array.map. -// Data is added +**Parameters:** +- `fn` `{Function}` - Function to transform each record +- `raw` `{Boolean}` - Whether to return raw data (default: `false`) -store.map(function (value) { - return value.property; -}); +**Returns:** `{Array}` Array of transformed results + +**Throws:** `{Error}` If fn is not a function + +```javascript +const names = store.map(record => record.name); +const summaries = store.map(record => ({ + id: record.id, + name: record.name, + email: record.email +})); ``` -### override(data[, type="records", fn]) -_Boolean_ +**See also:** filter(), forEach() -This is meant to be used in a paired override of the indexes & records, such that -you can avoid the `Promise` based code path of a `batch()` insert or `load()`. Accepts an optional third parameter to perform the -transformation to simplify cross domain issues. +#### merge(a, b, override) -Example of overriding a DataStore: -```javascript -const store = haro(); +Merges two values together with support for arrays and objects. -store.override({'field': {'value': ['pk']}}, "indexes"); +**Parameters:** +- `a` `{*}` - First value (target) +- `b` `{*}` - Second value (source) +- `override` `{Boolean}` - Whether to override arrays instead of concatenating (default: `false`) + +**Returns:** `{*}` Merged result + +```javascript +const merged = store.merge({a: 1}, {b: 2}); // {a: 1, b: 2} +const arrays = store.merge([1, 2], [3, 4]); // [1, 2, 3, 4] +const overridden = store.merge([1, 2], [3, 4], true); // [3, 4] ``` -### reduce(accumulator, value[, key, ctx=this, raw=false]) -_Array_ +#### override(data, type) -Runs an `Array.reduce()` inspired function against the data store (`Map`). +Replaces all store data or indexes with new data for bulk operations. -Example of filtering a DataStore: -```javascript -const store = haro(); +**Parameters:** +- `data` `{Array}` - Data to replace with +- `type` `{String}` - Type of data: `'records'` or `'indexes'` (default: `'records'`) -// Data is added +**Returns:** `{Boolean}` True if operation succeeded -store.reduce(function (accumulator, value, key) { - accumulator[key] = value; +**Throws:** `{Error}` If type is invalid - return accumulator; -}, {}); +```javascript +const backup = store.dump('records'); +// Later restore from backup +store.override(backup, 'records'); ``` +**See also:** dump(), clear() + +#### reduce(fn, accumulator) -### reindex([index]) -_Haro_ +Reduces all records to a single value using a reducer function. -Re-indexes the DataStore, to be called if changing the value of `index`. +**Parameters:** +- `fn` `{Function}` - Reducer function (accumulator, value, key, store) +- `accumulator` `{*}` - Initial accumulator value (default: `[]`) + +**Returns:** `{*}` Final reduced value -Example of mapping a DataStore: ```javascript -const store = haro(); +const totalAge = store.reduce((sum, record) => sum + record.age, 0); +const emailList = store.reduce((emails, record) => { + emails.push(record.email); + return emails; +}, []); +``` + +**See also:** map(), filter() + +#### reindex(index) + +Rebuilds indexes for specified fields or all fields for data consistency. -// Data is added +**Parameters:** +- `index` `{String|Array}` - Specific index field(s) to rebuild (optional) -// Creating a late index -store.reindex('field3'); +**Returns:** `{Haro}` Store instance for chaining -// Recreating indexes, this should only happen if the store is out of sync caused by developer code. -store.reindex(); +```javascript +store.reindex(); // Rebuild all indexes +store.reindex('name'); // Rebuild only name index +store.reindex(['name', 'email']); // Rebuild specific indexes ``` -### search(arg[, index=this.index, raw=false]) -_Array_ +#### search(value, index, raw) + +Searches for records containing a value across specified indexes. -Returns an `Array` of double `Arrays` with the shape `[key, value]` of records found matching `arg`. -If `arg` is a `Function` (parameters are `value` & `index`) a match is made if the result is `true`, if `arg` is a `RegExp` the field value must `.test()` -as `true`, else the value must be an identity match. The `index` parameter can be a `String` or `Array` of `Strings`; -if not supplied it defaults to `this.index`. +**Parameters:** +- `value` `{Function|RegExp|*}` - Value to search for +- `index` `{String|Array}` - Index(es) to search in (optional) +- `raw` `{Boolean}` - Whether to return raw data (default: `false`) -Indexed `Arrays` which are tested with a `RegExp` will be treated as a comma delimited `String`, e.g. `['hockey', 'football']` becomes `'hockey, football'` for the `RegExp`. +**Returns:** `{Array}` Array of matching records -Example of searching with a predicate function: ```javascript -const store = haro(null, {index: ['department', 'salary']}), - employees = [ - { name: 'Alice Johnson', department: 'Engineering', salary: 120000 }, - { name: 'Carlos Rivera', department: 'Design', salary: 95000 }, - { name: 'Priya Patel', department: 'Engineering', salary: 130000 } - ]; +// Function search +const results = store.search(key => key.includes('admin')); -store.batch(employees, 'set'); -// Find all employees in Engineering making over $125,000 -console.log(store.search((salary, department) => department === 'Engineering' && salary > 125000, ['salary', 'department'])); -// → [[$uuid, { name: 'Priya Patel', department: 'Engineering', salary: 130000 }]] +// Regex search on specific index +const nameResults = store.search(/^john/i, 'name'); + +// Value search across all indexes +const emailResults = store.search('gmail.com', 'email'); ``` -### set(key, data, batch=false, override=false) -_Object_ +**See also:** find(), where(), filter() + +#### set(key, data, batch, override) -Record in the DataStore. If `key` is `false` a version 4 `UUID` will be -generated. +Sets or updates a record in the store with automatic indexing. -If `override` is `true`, the existing record will be replaced instead of amended. +**Parameters:** +- `key` `{String|null}` - Key for the record, or null to use record's key field +- `data` `{Object}` - Record data to set (default: `{}`) +- `batch` `{Boolean}` - Whether this is part of a batch operation (default: `false`) +- `override` `{Boolean}` - Whether to override existing data instead of merging (default: `false`) + +**Returns:** `{Object}` The stored record -Example of creating a record: ```javascript -const store = haro(null, {key: 'id'}), - record = store.set(null, {id: 1, name: 'John Doe'}); +// Auto-generate key +const user = store.set(null, { name: 'John', age: 30 }); + +// Update existing record (merges by default) +const updated = store.set('user123', { age: 31 }); + +// Replace existing record completely +const replaced = store.set('user123', { name: 'Jane' }, false, true); +``` + +**See also:** get(), batch(), merge() + +#### sort(fn, frozen) + +Sorts all records using a comparator function. + +**Parameters:** +- `fn` `{Function}` - Comparator function for sorting (a, b) => number +- `frozen` `{Boolean}` - Whether to return frozen records (default: `false`) -console.log(record); // [1, {id: 1, name: 'Jane Doe'}] +**Returns:** `{Array}` Sorted array of records + +```javascript +const byAge = store.sort((a, b) => a.age - b.age); +const byName = store.sort((a, b) => a.name.localeCompare(b.name)); +const frozen = store.sort((a, b) => a.created - b.created, true); ``` -### sort(callbackFn, [frozen = true]) -_Array_ +**See also:** sortBy(), limit() + +#### sortBy(index, raw) + +Sorts records by a specific indexed field in ascending order. -Returns an Array of the DataStore, sorted by `callbackFn`. +**Parameters:** +- `index` `{String}` - Index field name to sort by +- `raw` `{Boolean}` - Whether to return raw data (default: `false`) + +**Returns:** `{Array}` Array of records sorted by the specified field + +**Throws:** `{Error}` If index field is empty or invalid -Example of sorting like an `Array`: ```javascript -const store = haro(null, {index: ['name', 'age']}), - data = [{name: 'John Doe', age: 30}, {name: 'Jane Doe', age: 28}]; +const byAge = store.sortBy('age'); +const byName = store.sortBy('name'); +const rawByDate = store.sortBy('created', true); +``` + +**See also:** sort(), find() -store.batch(data, 'set') -console.log(store.sort((a, b) => a < b ? -1 : (a > b ? 1 : 0))); // [{name: 'Jane Doe', age: 28}, {name: 'John Doe', age: 30}] +#### toArray() + +Converts all store data to a plain array of records. + +**Returns:** `{Array}` Array containing all records in the store + +```javascript +const allRecords = store.toArray(); +console.log(`Store contains ${allRecords.length} records`); ``` -### sortBy(index[, raw=false]) -_Array_ +**See also:** limit(), sort() + +#### uuid() -Returns an `Array` of double `Arrays` with the shape `[key, value]` of records sorted by an index. +Generates a RFC4122 v4 UUID for record identification. + +**Returns:** `{String}` UUID string in standard format -Example of sorting by an index: ```javascript -const store = haro(null, {index: ['priority', 'due']}), - tickets = [ - { title: 'Fix bug #42', priority: 2, due: '2025-05-18' }, - { title: 'Release v2.0', priority: 1, due: '2025-05-20' }, - { title: 'Update docs', priority: 3, due: '2025-05-22' } - ]; +const id = store.uuid(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479" +``` + +#### values() + +Returns an iterator of all values in the store. -store.batch(tickets, 'set'); -console.log(store.sortBy('priority')); -// → Sorted by priority ascending +**Returns:** `{Iterator}` Iterator of record values + +```javascript +for (const record of store.values()) { + console.log(record.name); +} ``` -### toArray([frozen=true]) -_Array_ +**See also:** keys(), entries() + +#### where(predicate, op) -Returns an Array of the DataStore. +Advanced filtering with predicate logic supporting AND/OR operations on arrays. + +**Parameters:** +- `predicate` `{Object}` - Object with field-value pairs for filtering +- `op` `{String}` - Operator for array matching: `'||'` for OR, `'&&'` for AND (default: `'||'`) + +**Returns:** `{Array}` Array of records matching the predicate criteria -Example of casting to an `Array`: ```javascript -const store = haro(), - notes = [ - { title: 'Call Alice', content: 'Discuss Q2 roadmap.' }, - { title: 'Email Carlos', content: 'Send project update.' } - ]; +// Find records with tags containing 'admin' OR 'user' +const users = store.where({ tags: ['admin', 'user'] }, '||'); + +// Find records with ALL specified tags +const powerUsers = store.where({ tags: ['admin', 'power'] }, '&&'); + +// Regex matching +const companyEmails = store.where({ email: /^[^@]+@company\.com$/ }); -store.batch(notes, 'set'); -console.log(store.toArray()); -// → [ -// { title: 'Call Alice', content: 'Discuss Q2 roadmap.' }, -// { title: 'Email Carlos', content: 'Send project update.' } -// ] +// Array field matching +const multiDeptUsers = store.where({ departments: ['IT', 'HR'] }); ``` -### values() -_MapIterator_ +**See also:** find(), filter(), search() -Returns a new `Iterator` object that contains the values for each element in the `Map` object in insertion order. +## Lifecycle Hooks + +Override these methods in subclasses for custom behavior: + +### beforeBatch(args, type) +Executed before batch operations for preprocessing. + +### beforeClear() +Executed before clear operation for cleanup preparation. + +### beforeDelete(key, batch) +Executed before delete operation for validation or logging. + +### beforeSet(key, data, batch, override) +Executed before set operation for data validation or transformation. + +### onbatch(results, type) +Executed after batch operations for postprocessing. + +### onclear() +Executed after clear operation for cleanup tasks. + +### ondelete(key, batch) +Executed after delete operation for logging or notifications. + +### onset(record, batch) +Executed after set operation for indexing or event emission. + +## Examples + +### User Management System -Example of iterating the values: ```javascript -const store = haro(), - data = [{name: 'John Doe', age: 30}, {name: 'Jane Doe', age: 28}]; +import { haro } from 'haro'; + +const users = haro(null, { + index: ['email', 'department', 'role', 'department|role'], + key: 'id', + versioning: true, + immutable: true +}); + +// Add users with batch operation +users.batch([ + { + id: 'u1', + email: 'alice@company.com', + name: 'Alice Johnson', + department: 'Engineering', + role: 'Senior Developer', + active: true + }, + { + id: 'u2', + email: 'bob@company.com', + name: 'Bob Smith', + department: 'Engineering', + role: 'Team Lead', + active: true + }, + { + id: 'u3', + email: 'carol@company.com', + name: 'Carol Davis', + department: 'Marketing', + role: 'Manager', + active: false + } +], 'set'); + +// Find by department +const engineers = users.find({ department: 'Engineering' }); + +// Complex queries with where() +const activeEngineers = users.where({ + department: 'Engineering', + active: true +}, '&&'); -store.batch(data, 'set') +// Search across multiple fields +const managers = users.search(/manager|lead/i, ['role']); -const iterator = store.values(); -let item = iterator.next(); +// Pagination for large datasets +const page1 = users.limit(0, 10); +const page2 = users.limit(10, 10); -while (!item.done) { - console.log(item.value); - item = iterator.next(); -}; +// Update user with version tracking +const updated = users.set('u1', { role: 'Principal Developer' }); +console.log(users.versions.get('u1')); // Previous versions ``` -### where(predicate[, raw=false, op="||"]) -_Array_ +### E-commerce Product Catalog -Ideal for when dealing with a composite index which contains an `Array` of values, which would make matching on a single value impossible when using `find()`. +```javascript +import { Haro } from 'haro'; + +class ProductCatalog extends Haro { + constructor() { + super({ + index: ['category', 'brand', 'price', 'tags', 'category|brand'], + key: 'sku', + versioning: true + }); + } + + beforeSet(key, data, batch, override) { + // Validate required fields + if (!data.name || !data.price || !data.category) { + throw new Error('Missing required product fields'); + } + + // Normalize price + if (typeof data.price === 'string') { + data.price = parseFloat(data.price); + } + + // Auto-generate SKU if not provided + if (!data.sku && !key) { + data.sku = this.generateSKU(data); + } + } + + onset(record, batch) { + console.log(`Product ${record.name} (${record.sku}) updated`); + } + + generateSKU(product) { + const prefix = product.category.substring(0, 3).toUpperCase(); + const suffix = Date.now().toString().slice(-6); + return `${prefix}-${suffix}`; + } + + // Custom business methods + findByPriceRange(min, max) { + return this.filter(product => + product.price >= min && product.price <= max + ); + } + + searchProducts(query) { + // Search across multiple fields + const lowerQuery = query.toLowerCase(); + return this.filter(product => + product.name.toLowerCase().includes(lowerQuery) || + product.description.toLowerCase().includes(lowerQuery) || + product.tags.some(tag => tag.toLowerCase().includes(lowerQuery)) + ); + } + + getRecommendations(sku, limit = 5) { + const product = this.get(sku); + if (!product) return []; + + // Find similar products by category and brand + return this.find({ + category: product.category, + brand: product.brand + }) + .filter(p => p.sku !== sku) + .slice(0, limit); + } +} + +const catalog = new ProductCatalog(); + +// Add products +catalog.batch([ + { + sku: 'LAP-001', + name: 'MacBook Pro 16"', + category: 'Laptops', + brand: 'Apple', + price: 2499.99, + tags: ['professional', 'high-performance', 'creative'], + description: 'Powerful laptop for professionals' + }, + { + sku: 'LAP-002', + name: 'ThinkPad X1 Carbon', + category: 'Laptops', + brand: 'Lenovo', + price: 1899.99, + tags: ['business', 'lightweight', 'durable'], + description: 'Business laptop with excellent build quality' + } +], 'set'); + +// Business queries +const laptops = catalog.find({ category: 'Laptops' }); +const affordable = catalog.findByPriceRange(1000, 2000); +const searchResults = catalog.searchProducts('professional'); +const recommendations = catalog.getRecommendations('LAP-001'); +``` + +### Real-time Analytics Dashboard ```javascript -const store = haro(null, {key: 'guid', index: ['name', 'name|age', 'age']}), - data = [{guid: 'abc', name: 'John Doe', age: 30}, {guid: 'def', name: 'Jane Doe', age: 28}]; +import { haro } from 'haro'; + +// Event tracking store +const events = haro(null, { + index: ['type', 'userId', 'timestamp', 'type|userId'], + key: 'id', + immutable: false // Allow mutations for performance +}); + +// Session tracking store +const sessions = haro(null, { + index: ['userId', 'status', 'lastActivity'], + key: 'sessionId', + versioning: true +}); + +// Analytics functions +function trackEvent(type, userId, data = {}) { + return events.set(null, { + id: events.uuid(), + type, + userId, + timestamp: Date.now(), + data, + ...data + }); +} -store.batch(data, 'set'); -console.log(store.where({name: 'John Doe', age: 30})); // [{guid: 'abc', name: 'John Doe', age: 30}] +function getActiveUsers(minutes = 5) { + const threshold = Date.now() - (minutes * 60 * 1000); + return sessions.filter(session => + session.status === 'active' && + session.lastActivity > threshold + ); +} + +function getUserActivity(userId, hours = 24) { + const since = Date.now() - (hours * 60 * 60 * 1000); + return events.find({ userId }) + .filter(event => event.timestamp > since) + .sort((a, b) => b.timestamp - a.timestamp); +} + +function getEventStats(timeframe = 'hour') { + const now = Date.now(); + const intervals = { + hour: 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000 + }; + + const since = now - intervals[timeframe]; + const recentEvents = events.filter(event => event.timestamp > since); + + return recentEvents.reduce((stats, event) => { + stats[event.type] = (stats[event.type] || 0) + 1; + return stats; + }, {}); +} + +// Usage +trackEvent('page_view', 'user123', { page: '/dashboard' }); +trackEvent('click', 'user123', { element: 'nav-menu' }); +trackEvent('search', 'user456', { query: 'analytics' }); + +console.log('Active users:', getActiveUsers().length); +console.log('User activity:', getUserActivity('user123')); +console.log('Event stats:', getEventStats('hour')); ``` -## Contributing +### Configuration Management + +```javascript +import { Haro } from 'haro'; + +class ConfigStore extends Haro { + constructor() { + super({ + index: ['environment', 'service', 'type', 'environment|service'], + key: 'key', + versioning: true, + immutable: true + }); + + this.loadDefaults(); + } + + loadDefaults() { + this.batch([ + { key: 'db.host', value: 'localhost', environment: 'dev', type: 'database' }, + { key: 'db.port', value: 5432, environment: 'dev', type: 'database' }, + { key: 'api.timeout', value: 30000, environment: 'dev', type: 'api' }, + { key: 'db.host', value: 'prod-db.example.com', environment: 'prod', type: 'database' }, + { key: 'db.port', value: 5432, environment: 'prod', type: 'database' }, + { key: 'api.timeout', value: 10000, environment: 'prod', type: 'api' } + ], 'set'); + } + + getConfig(key, environment = 'dev') { + const configs = this.find({ key, environment }); + return configs.length > 0 ? configs[0].value : null; + } + + getEnvironmentConfig(environment) { + return this.find({ environment }).reduce((config, item) => { + config[item.key] = item.value; + return config; + }, {}); + } + + updateConfig(key, value, environment = 'dev') { + const existing = this.find({ key, environment })[0]; + if (existing) { + return this.set(key, { ...existing, value }); + } else { + return this.set(key, { key, value, environment, type: 'custom' }); + } + } + + getDatabaseConfig(environment = 'dev') { + return this.find({ environment, type: 'database' }); + } +} -Contributions, issues, and feature requests are welcome! Feel free to check the [issues page](https://github.com/avoidwork/haro/issues) or submit a pull request. +const config = new ConfigStore(); -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/my-feature`) -3. Commit your changes (`git commit -am 'Add new feature'`) -4. Push to the branch (`git push origin feature/my-feature`) -5. Open a pull request +// Get specific config +console.log(config.getConfig('db.host', 'prod')); // 'prod-db.example.com' -## Support +// Get all configs for environment +const devConfig = config.getEnvironmentConfig('dev'); -For questions, suggestions, or support, please open an issue on [GitHub](https://github.com/avoidwork/haro/issues), or contact the maintainer. +// Update configuration +config.updateConfig('api.timeout', 45000, 'dev'); -## License +// Get configuration history +console.log(config.versions.get('api.timeout')); +``` -This project is licensed under the BSD-3 license - see the [LICENSE](./LICENSE) file for details. +## Performance -## Changelog +Haro is optimized for: +- **Fast indexing**: O(1) lookups on indexed fields +- **Efficient searches**: Regex and function-based filtering with index acceleration +- **Memory efficiency**: Minimal overhead with optional immutability +- **Batch operations**: Optimized bulk inserts and updates +- **Version tracking**: Efficient MVCC-style versioning when enabled + +### Performance Characteristics + +| Operation | Indexed | Non-Indexed | Notes | +|-----------|---------|-------------|-------| +| `find()` | O(1) | O(n) | Use indexes for best performance | +| `get()` | O(1) | O(1) | Direct key lookup | +| `set()` | O(1) | O(1) | Includes index updates | +| `delete()` | O(1) | O(1) | Includes index cleanup | +| `filter()` | O(n) | O(n) | Full scan with predicate | +| `search()` | O(k) | O(n) | k = matching index entries | + +## License -See [CHANGELOG.md](./CHANGELOG.md) for release notes and version history. +Copyright (c) 2025 Jason Mulligan +Licensed under the BSD-3-Clause license. diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 00000000..d32bf6e2 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,600 @@ +# Haro Benchmark Suite + +A comprehensive performance testing suite for the Haro immutable data store library. This benchmark suite tests various aspects of Haro's performance including basic operations, search/filter capabilities, indexing, memory usage, and comparisons with native JavaScript data structures. + +## Overview + +The benchmark suite consists of several modules that test different aspects of Haro's performance: + +- **Basic Operations** - CRUD operations (Create, Read, Update, Delete) +- **Search & Filter** - Query performance with various patterns +- **Index Operations** - Indexing performance and benefits +- **Memory Usage** - Memory consumption patterns and efficiency +- **Comparison** - Performance vs native JavaScript structures +- **Utility Operations** - Helper methods (clone, merge, freeze, forEach, uuid) +- **Pagination** - Limit-based pagination performance +- **Persistence** - Dump/override operations for data serialization +- **Immutable Comparison** - Performance comparison between mutable and immutable modes + +## Quick Start + +### Run All Benchmarks + +```bash +node benchmarks/index.js +``` + +### Run Specific Benchmark Categories + +```bash +# Run only basic operations +node benchmarks/index.js --basic-only + +# Run only search and filter benchmarks +node benchmarks/index.js --search-only + +# Run only index operations +node benchmarks/index.js --index-only + +# Run only memory usage benchmarks +node benchmarks/index.js --memory-only + +# Run only comparison benchmarks +node benchmarks/index.js --comparison-only + +# Run only utility operations benchmarks +node benchmarks/index.js --utilities-only + +# Run only pagination benchmarks +node benchmarks/index.js --pagination-only + +# Run only persistence benchmarks +node benchmarks/index.js --persistence-only + +# Run only immutable vs mutable comparison +node benchmarks/index.js --immutable-only + +# Run only core benchmarks (basic, search, index) +node benchmarks/index.js --core-only + +# Run only advanced benchmarks (memory, comparison, utilities, etc.) +node benchmarks/index.js --advanced-only + +# Exclude specific benchmarks +node benchmarks/index.js --no-memory --no-persistence + +# Run quietly (minimal output) +node benchmarks/index.js --quiet +``` + +### Run Individual Benchmark Files + +```bash +# Basic operations +node benchmarks/basic-operations.js + +# Search and filter operations +node benchmarks/search-filter.js + +# Index operations +node benchmarks/index-operations.js + +# Memory usage analysis +node benchmarks/memory-usage.js + +# Performance comparisons +node benchmarks/comparison.js + +# Utility operations +node benchmarks/utility-operations.js + +# Pagination benchmarks +node benchmarks/pagination.js + +# Persistence operations +node benchmarks/persistence.js + +# Immutable vs mutable comparison +node benchmarks/immutable-comparison.js +``` + +## Benchmark Categories + +### 1. Basic Operations (`basic-operations.js`) + +Tests fundamental CRUD operations performance: + +- **SET operations**: Individual and batch record creation +- **GET operations**: Record retrieval by key +- **DELETE operations**: Individual and batch record deletion +- **CLEAR operations**: Store clearing performance +- **Utility operations**: `toArray()`, `keys()`, `values()`, `entries()` + +**Data Sizes Tested**: 100, 1,000, 10,000, 50,000 records + +**Key Metrics**: +- Operations per second +- Total execution time +- Average operation time + +### 2. Search & Filter (`search-filter.js`) + +Tests query performance with various patterns: + +- **FIND operations**: Indexed field queries +- **FILTER operations**: Predicate-based filtering +- **SEARCH operations**: String, regex, and function-based search +- **WHERE operations**: Complex conditional queries with AND/OR +- **MAP/REDUCE operations**: Data transformation performance +- **SORT operations**: Sorting by different criteria + +**Data Sizes Tested**: 1,000, 10,000, 50,000 records + +**Key Features Tested**: +- Simple vs complex queries +- Indexed vs non-indexed queries +- Array field queries +- Regular expression matching +- Custom predicate functions + +### 3. Index Operations (`index-operations.js`) + +Tests indexing performance and benefits: + +- **Index creation**: Single and composite index building +- **Index queries**: Performance of indexed vs non-indexed queries +- **Index modification**: Performance impact of updates/deletes +- **Index memory**: Memory overhead and export/import +- **Index comparison**: Performance benefits analysis + +**Index Types Tested**: +- Single field indexes +- Composite indexes (multi-field) +- Array field indexes +- Nested field indexes + +**Data Sizes Tested**: 1,000, 10,000, 50,000 records + +### 4. Memory Usage (`memory-usage.js`) + +Analyzes memory consumption patterns: + +- **Creation memory**: Memory usage during store creation +- **Operation memory**: Memory impact of CRUD operations +- **Query memory**: Memory consumption during queries +- **Index memory**: Memory overhead of indexing +- **Versioning memory**: Memory impact of versioning +- **Stress memory**: Memory under high load conditions + +**Special Features**: +- Memory growth analysis over time +- Garbage collection tracking +- Memory leak detection +- Memory efficiency recommendations + +### 5. Comparison (`comparison.js`) + +Compares Haro performance with native JavaScript structures: + +- **vs Map**: Performance comparison with native Map +- **vs Object**: Performance comparison with plain objects +- **vs Array**: Performance comparison with native arrays +- **Advanced features**: Unique Haro capabilities vs manual implementation + +**Operations Compared**: +- Storage operations +- Retrieval operations +- Query operations +- Deletion operations +- Aggregation operations +- Sorting operations +- Memory usage + +### 6. Utility Operations (`utility-operations.js`) + +Tests performance of helper and utility methods: + +- **CLONE operations**: Deep cloning of objects and arrays +- **MERGE operations**: Object and array merging with different strategies +- **FREEZE operations**: Object freezing for immutability +- **forEach operations**: Iteration with different callback complexities +- **UUID operations**: UUID generation and uniqueness testing + +**Data Sizes Tested**: 100, 1,000, 5,000 records + +**Key Features Tested**: +- Simple vs complex object cloning +- Array vs object merging strategies +- Performance vs safety trade-offs +- UUID generation rates and uniqueness + +### 7. Pagination (`pagination.js`) + +Tests pagination and data limiting performance: + +- **LIMIT operations**: Basic pagination with different page sizes +- **OFFSET operations**: Performance across different offset positions +- **PAGE SIZE optimization**: Finding optimal page sizes +- **SEQUENTIAL pagination**: Simulating real browsing patterns +- **COMBINED operations**: Pagination with filtering and sorting + +**Data Sizes Tested**: 1,000, 10,000, 50,000 records + +**Key Features Tested**: +- Small vs large page sizes +- First page vs middle vs last page performance +- Memory efficiency of chunked vs full data access +- Integration with query operations + +### 8. Persistence (`persistence.js`) + +Tests data serialization and restoration performance: + +- **DUMP operations**: Exporting records and indexes +- **OVERRIDE operations**: Importing and restoring data +- **ROUND-TRIP operations**: Complete export/import cycles +- **COMPLEX objects**: Performance with nested data structures +- **MEMORY efficiency**: Memory usage during persistence operations + +**Data Sizes Tested**: 100, 1,000, 5,000 records + +**Key Features Tested**: +- Records vs indexes export/import +- Data integrity validation +- Memory impact of persistence operations +- Complex object serialization performance + +### 9. Immutable Comparison (`immutable-comparison.js`) + +Compares performance between immutable and mutable modes: + +- **STORE CREATION**: Setup performance comparison +- **CRUD operations**: Create, Read, Update, Delete in both modes +- **QUERY operations**: Find, filter, search, where performance +- **TRANSFORMATION**: Map, reduce, sort, forEach comparison +- **MEMORY usage**: Memory consumption patterns +- **DATA SAFETY**: Mutation protection analysis + +**Data Sizes Tested**: 100, 1,000, 5,000 records + +**Key Features Tested**: +- Performance vs safety trade-offs +- Memory overhead of immutable mode +- Operation-specific performance differences +- Data protection effectiveness + +## Latest Benchmark Results + +### Performance Summary (Last Updated: December 2024) + +**Overall Test Results:** +- **Total Tests**: 572 tests across 9 categories +- **Total Runtime**: 1.6 minutes +- **Test Environment**: Node.js on macOS (darwin 24.5.0) + +**Performance Highlights:** +- **Fastest Operation**: HAS operation (20,815,120 ops/second on 1,000 records) +- **Slowest Operation**: BATCH SET (88 ops/second on 50,000 records) +- **Memory Efficiency**: Most efficient DELETE operations (-170.19 MB for 100 deletions) +- **Least Memory Efficient**: FIND operations (34.49 MB for 25,000 records with 100 queries) + +### Category Performance Breakdown + +#### Basic Operations +- **Tests**: 40 tests +- **Runtime**: 249ms +- **Average Performance**: 3,266,856 ops/second +- **Key Findings**: Excellent performance for core CRUD operations + +#### Search & Filter Operations +- **Tests**: 93 tests +- **Runtime**: 1.2 minutes +- **Average Performance**: 856,503 ops/second +- **Key Findings**: Strong performance for indexed queries, good filter performance + +#### Index Operations +- **Tests**: 60 tests +- **Runtime**: 2.1 seconds +- **Average Performance**: 386,859 ops/second +- **Key Findings**: Efficient index creation and maintenance + +#### Memory Usage +- **Tests**: 60 tests +- **Runtime**: 419ms +- **Average Memory**: 1.28 MB +- **Key Findings**: Efficient memory usage patterns + +#### Comparison with Native Structures +- **Tests**: 93 tests +- **Runtime**: 12.6 seconds +- **Average Performance**: 2,451,027 ops/second +- **Key Findings**: Competitive with native structures considering feature richness + +#### Utility Operations +- **Tests**: 45 tests +- **Runtime**: 206ms +- **Average Performance**: 3,059,333 ops/second +- **Key Findings**: Excellent performance for clone, merge, freeze operations + +#### Pagination +- **Tests**: 65 tests +- **Runtime**: 579ms +- **Average Performance**: 100,162 ops/second +- **Key Findings**: Efficient pagination suitable for UI requirements + +#### Persistence +- **Tests**: 38 tests +- **Runtime**: 314ms +- **Average Performance**: 114,384 ops/second +- **Key Findings**: Good performance for data serialization/deserialization + +#### Immutable vs Mutable Comparison +- **Tests**: 78 tests +- **Runtime**: 8.4 seconds +- **Average Performance**: 835,983 ops/second +- **Key Findings**: Minimal performance difference for most operations + +### Detailed Performance Results + +#### Basic Operations Performance +- **SET operations**: Up to 3.2M ops/sec for typical workloads +- **GET operations**: Up to 20M ops/sec with index lookups +- **DELETE operations**: Efficient cleanup with index maintenance +- **HAS operations**: 20,815,120 ops/sec (best performer) +- **CLEAR operations**: Fast bulk deletion +- **BATCH operations**: Optimized for bulk data manipulation + +#### Query Operations Performance +- **FIND (indexed)**: 64,594 ops/sec (1,000 records) +- **FILTER operations**: 46,255 ops/sec +- **SEARCH operations**: Strong regex and text search performance +- **WHERE clauses**: 60,710 ops/sec for complex queries +- **SORT operations**: Efficient sorting with index optimization + +#### Comparison with Native Structures +- **Haro vs Array Filter**: 46,255 vs 189,293 ops/sec +- **Haro vs Map**: Comparable performance for basic operations +- **Haro vs Object**: Trade-off between features and raw performance +- **Advanced Features**: Unique capabilities not available in native structures + +#### Memory Usage Analysis +- **Haro (50,000 records)**: 13.98 MB +- **Map (50,000 records)**: 3.52 MB +- **Object (50,000 records)**: 1.27 MB +- **Array (50,000 records)**: 0.38 MB +- **Overhead Analysis**: Reasonable for feature set provided + +#### Utility Operations Performance +- **Clone simple objects**: 1,605,780 ops/sec +- **Clone complex objects**: 234,455 ops/sec +- **Merge operations**: Up to 2,021,394 ops/sec +- **Freeze operations**: Up to 17,316,017 ops/sec +- **forEach operations**: Up to 58,678 ops/sec +- **UUID generation**: 14,630,218 ops/sec + +#### Pagination Performance +- **Small pages (10 items)**: 616,488 ops/sec +- **Medium pages (50 items)**: 271,554 ops/sec +- **Large pages (100 items)**: 153,433 ops/sec +- **Sequential pagination**: Efficient for typical UI patterns + +#### Immutable vs Mutable Performance +- **Creation**: Minimal difference (1.27x faster mutable) +- **Read operations**: Comparable performance +- **Write operations**: Slight advantage to mutable mode +- **Transformation operations**: Significant cost in immutable mode + +### Performance Recommendations + +Based on the latest benchmark results: + +1. **✅ Basic operations performance is excellent** for most use cases +2. **✅ Memory usage is efficient** for typical workloads +3. **📊 Review comparison results** to understand trade-offs vs native structures +4. **✅ Utility operations** (clone, merge, freeze) perform well +5. **✅ Pagination performance** is suitable for typical UI requirements +6. **💾 Persistence operations** available for data serialization needs +7. **🔒 Review immutable vs mutable comparison** for data safety vs performance trade-offs + +## Understanding Results + +### Performance Metrics + +- **Operations per Second**: Higher is better +- **Total Time**: Time to complete all iterations +- **Average Time**: Time per single operation +- **Memory Delta**: Memory usage change (MB) + +### Performance Indicators + +- **✅ Excellent**: > 100,000 ops/second +- **🟡 Good**: 10,000 - 100,000 ops/second +- **🟠 Moderate**: 1,000 - 10,000 ops/second +- **🔴 Slow**: < 1,000 ops/second + +### Memory Indicators + +- **✅ Efficient**: < 10 MB for typical operations +- **🟡 Moderate**: 10-50 MB +- **🟠 High**: 50-100 MB +- **🔴 Excessive**: > 100 MB + +## Performance Analysis & Insights + +### Key Performance Insights + +Based on the latest benchmark results, here are the key insights: + +#### Performance Strengths +1. **Excellent Basic Operations**: Core CRUD operations perform exceptionally well (3.2M+ ops/sec) +2. **Fast Record Lookups**: HAS operations achieve 20M+ ops/sec, demonstrating efficient key-based access +3. **Efficient Indexing**: Index-based queries provide significant performance benefits +4. **Strong Utility Performance**: Clone, merge, and freeze operations are highly optimized +5. **Competitive with Native Structures**: Maintains competitive performance while providing rich features + +#### Performance Considerations +1. **Memory Overhead**: ~10x memory usage compared to native Arrays but justified by features +2. **Filter vs Find**: Array filters are ~4x faster than Haro filters, but Haro provides more features +3. **Immutable Mode Cost**: Transformation operations in immutable mode show significant performance impact +4. **Batch Operations**: Essential for bulk data manipulation at scale +5. **Complex Queries**: WHERE clauses maintain good performance even with multiple conditions + +#### Scaling Characteristics +- **Small datasets (100-1K records)**: Excellent performance across all operations +- **Medium datasets (1K-10K records)**: Very good performance with minor degradation +- **Large datasets (10K-50K records)**: Good performance with more noticeable costs for complex operations +- **Memory scaling**: Linear growth with reasonable efficiency + +### Performance Recommendations by Use Case + +#### High-Performance Applications +- Use mutable mode for maximum performance +- Leverage indexed queries (find) over filters +- Implement batch operations for bulk changes +- Consider pagination for large result sets +- Monitor memory usage with large datasets + +#### Data-Safe Applications +- Use immutable mode for data integrity +- Accept performance trade-offs for safety +- Use utility methods (clone, merge) for safe data manipulation +- Enable versioning only when needed +- Consider persistence for backup/restore needs + +#### Mixed Workloads +- Profile your specific use case +- Consider hybrid approaches (mutable for writes, immutable for reads) +- Use indexes strategically +- Implement proper pagination +- Monitor and optimize memory usage + +## Advanced Usage + +### Memory Profiling + +For memory benchmarks, run with garbage collection enabled: + +```bash +node --expose-gc benchmarks/memory-usage.js +``` + +### Custom Data Sizes + +Modify the `dataSizes` array in each benchmark file to test different data volumes: + +```javascript +// For basic operations and queries +const dataSizes = [100, 1000, 10000, 50000, 100000]; + +// For memory-intensive tests +const dataSizes = [100, 1000, 5000]; + +// For complex operations like persistence +const dataSizes = [50, 500, 2000]; +``` + +### Performance Optimization + +Based on the latest benchmark results, consider these optimizations: + +1. **Use indexed queries** (`find()`) instead of filters for better performance (64K vs 46K ops/sec) +2. **Create composite indexes** for multi-field queries +3. **Use batch operations** for bulk data operations +4. **Enable versioning** only when needed (impacts performance) +5. **Consider memory limits** for large datasets (13.98MB for 50K records) +6. **Use immutable mode** strategically for data safety vs performance +7. **Implement pagination** for large result sets using `limit()` (616K ops/sec for small pages) +8. **Use utility methods** (clone: 1.6M ops/sec, merge: 2M ops/sec) for safe data manipulation +9. **Consider persistence** for data backup and restoration needs (114K ops/sec) +10. **Optimize WHERE queries** with proper indexing and operators + +## Interpreting Results + +### When to Use Haro + +Haro is ideal when you need: +- **Complex queries** with multiple conditions (WHERE clauses: 60K ops/sec) +- **Indexed search** performance (FIND: 64K ops/sec) +- **Immutable data** with transformation capabilities +- **Versioning** and data history tracking +- **Advanced features** like regex search, array queries, pagination +- **Memory efficiency** is acceptable for feature richness +- **Utility operations** for safe data manipulation + +### When to Use Native Structures + +Consider native structures when: +- **Simple key-value** operations dominate (Array filter: 189K ops/sec) +- **Memory efficiency** is critical (Array: 0.38MB vs Haro: 13.98MB for 50K records) +- **Maximum performance** for basic operations is needed +- **Minimal overhead** is required +- **No advanced querying** features needed + +### Performance vs Feature Trade-offs + +| Feature | Performance Impact | Recommendation | +|---------|-------------------|----------------| +| Indexing | ✅ Significant improvement | Always use for queried fields | +| Immutable Mode | 🟡 Mixed (read: good, transform: slow) | Use for data safety when needed | +| Versioning | 🟡 Moderate impact | Enable only when history tracking required | +| Batch Operations | ✅ Better for bulk operations | Use for multiple changes | +| Pagination | ✅ Efficient for large datasets | Implement for UI performance | +| Persistence | 🟡 Good for data backup | Use for serialization needs | + +## Contributing + +To add new benchmarks: + +1. Create a new benchmark file in the `benchmarks/` directory +2. Follow the existing pattern with JSDoc comments +3. Export a main function that runs the benchmarks +4. Add the benchmark to `index.js` if needed + +### Benchmark Structure + +```javascript +/** + * Benchmark function description + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkFeature(dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const result = benchmark('Test name', () => { + // Test code here + }); + results.push(result); + }); + + return results; +} +``` + +## System Requirements + +- Node.js 16.7.0 or higher +- Minimum 4GB RAM for full benchmark suite +- Adequate disk space for test data generation + +## Troubleshooting + +### Common Issues + +1. **Out of Memory**: Reduce data sizes or run individual benchmarks +2. **Slow Performance**: Ensure no other processes are competing for resources +3. **Inconsistent Results**: Run multiple times and average the results + +### Performance Factors + +Results may vary based on: +- System specifications (CPU, RAM) +- Node.js version +- Other running processes +- System load +- V8 engine optimizations + +## License + +This benchmark suite is part of the Haro project and follows the same license terms. \ No newline at end of file diff --git a/benchmarks/basic-operations.js b/benchmarks/basic-operations.js new file mode 100644 index 00000000..076ce057 --- /dev/null +++ b/benchmarks/basic-operations.js @@ -0,0 +1,248 @@ +import { performance } from "node:perf_hooks"; +import { haro } from "../dist/haro.js"; + +/** + * Generates test data for benchmarking + * @param {number} size - Number of records to generate + * @returns {Array} Array of test records + */ +function generateTestData (size) { + const data = []; + for (let i = 0; i < size; i++) { + data.push({ + id: i, + name: `User ${i}`, + email: `user${i}@example.com`, + age: Math.floor(Math.random() * 50) + 18, + department: `Dept ${i % 10}`, + active: Math.random() > 0.5, + tags: [`tag${i % 5}`, `tag${i % 3}`], + metadata: { + created: new Date(), + score: Math.random() * 100, + level: Math.floor(Math.random() * 10) + } + }); + } + + return data; +} + +/** + * Runs a benchmark test and returns timing information + * @param {string} name - Name of the test + * @param {Function} fn - Function to benchmark + * @param {number} iterations - Number of iterations to run + * @returns {Object} Benchmark results + */ +function benchmark (name, fn, iterations = 1000) { + const start = performance.now(); + for (let i = 0; i < iterations; i++) { + fn(); + } + const end = performance.now(); + const total = end - start; + const avgTime = total / iterations; + + return { + name, + iterations, + totalTime: total, + avgTime, + opsPerSecond: Math.floor(1000 / avgTime) + }; +} + +/** + * Benchmarks basic SET operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkSetOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateTestData(size); + const store = haro(); + + // Individual set operations + const setResult = benchmark(`SET (${size} records)`, () => { + const record = testData[Math.floor(Math.random() * testData.length)]; + store.set(record.id, record); + }); + results.push(setResult); + + // Batch set operations + const batchStore = haro(); + const batchResult = benchmark(`BATCH SET (${size} records)`, () => { + batchStore.batch(testData, "set"); + }, 1); + results.push(batchResult); + }); + + return results; +} + +/** + * Benchmarks basic GET operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkGetOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateTestData(size); + const store = haro(testData); + + // Random get operations + const getResult = benchmark(`GET (${size} records)`, () => { + const id = Math.floor(Math.random() * size); + store.get(id); + }); + results.push(getResult); + + // Has operations + const hasResult = benchmark(`HAS (${size} records)`, () => { + const id = Math.floor(Math.random() * size); + store.has(id); + }); + results.push(hasResult); + }); + + return results; +} + +/** + * Benchmarks DELETE operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkDeleteOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateTestData(size); + + // Individual delete operations + const deleteStore = haro(testData); + const deleteResult = benchmark(`DELETE (${size} records)`, () => { + const keys = Array.from(deleteStore.keys()); + if (keys.length > 0) { + const randomKey = keys[Math.floor(Math.random() * keys.length)]; + try { + deleteStore.del(randomKey); + } catch (e) { // eslint-disable-line no-unused-vars + // Record might already be deleted + } + } + }, Math.min(100, size)); + results.push(deleteResult); + + // Clear operations + const clearStore = haro(testData); + const clearResult = benchmark(`CLEAR (${size} records)`, () => { + clearStore.clear(); + clearStore.batch(testData, "set"); + }, 10); + results.push(clearResult); + }); + + return results; +} + +/** + * Benchmarks utility operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkUtilityOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateTestData(size); + const store = haro(testData); + + // ToArray operations + const toArrayResult = benchmark(`toArray (${size} records)`, () => { + store.toArray(); + }, 100); + results.push(toArrayResult); + + // Keys operations + const keysResult = benchmark(`keys (${size} records)`, () => { + Array.from(store.keys()); + }, 100); + results.push(keysResult); + + // Values operations + const valuesResult = benchmark(`values (${size} records)`, () => { + Array.from(store.values()); + }, 100); + results.push(valuesResult); + + // Entries operations + const entriesResult = benchmark(`entries (${size} records)`, () => { + Array.from(store.entries()); + }, 100); + results.push(entriesResult); + }); + + return results; +} + +/** + * Prints benchmark results in a formatted table + * @param {Array} results - Array of benchmark results + */ +function printResults (results) { + console.log("\n=== BASIC OPERATIONS BENCHMARK RESULTS ===\n"); + + console.log("Operation".padEnd(30) + "Iterations".padEnd(12) + "Total Time (ms)".padEnd(18) + "Avg Time (ms)".padEnd(16) + "Ops/Second"); + console.log("-".repeat(88)); + + results.forEach(result => { + const name = result.name.padEnd(30); + const iterations = result.iterations.toString().padEnd(12); + const totalTime = result.totalTime.toFixed(2).padEnd(18); + const avgTime = result.avgTime.toFixed(4).padEnd(16); + const opsPerSecond = result.opsPerSecond.toLocaleString(); + + console.log(name + iterations + totalTime + avgTime + opsPerSecond); + }); + + console.log("\n"); +} + +/** + * Main function to run all basic operations benchmarks + */ +function runBasicOperationsBenchmarks () { + console.log("🚀 Running Basic Operations Benchmarks...\n"); + + const dataSizes = [100, 1000, 10000, 50000]; + const allResults = []; + + console.log("Testing SET operations..."); + allResults.push(...benchmarkSetOperations(dataSizes)); + + console.log("Testing GET operations..."); + allResults.push(...benchmarkGetOperations(dataSizes)); + + console.log("Testing DELETE operations..."); + allResults.push(...benchmarkDeleteOperations(dataSizes)); + + console.log("Testing utility operations..."); + allResults.push(...benchmarkUtilityOperations(dataSizes)); + + printResults(allResults); + + return allResults; +} + +// Run benchmarks if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runBasicOperationsBenchmarks(); +} + +export { runBasicOperationsBenchmarks, generateTestData }; diff --git a/benchmarks/comparison.js b/benchmarks/comparison.js new file mode 100644 index 00000000..e50659c9 --- /dev/null +++ b/benchmarks/comparison.js @@ -0,0 +1,601 @@ +import { performance } from "node:perf_hooks"; +import { haro } from "../dist/haro.js"; +import { generateIndexTestData } from "./index-operations.js"; + +/** + * Runs a benchmark test and returns timing information + * @param {string} name - Name of the test + * @param {Function} fn - Function to benchmark + * @param {number} iterations - Number of iterations to run + * @returns {Object} Benchmark results + */ +function benchmark (name, fn, iterations = 1000) { + const start = performance.now(); + for (let i = 0; i < iterations; i++) { + fn(); + } + const end = performance.now(); + const total = end - start; + const avgTime = total / iterations; + + return { + name, + iterations, + totalTime: total, + avgTime, + opsPerSecond: Math.floor(1000 / avgTime) + }; +} + +/** + * Benchmarks basic storage operations comparison + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkStorageComparison (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Haro storage + const haroSetResult = benchmark(`Haro SET (${size} records)`, () => { + const store = haro(); + testData.forEach(record => store.set(record.id, record)); + }, 10); + results.push(haroSetResult); + + // Native Map storage + const mapSetResult = benchmark(`Map SET (${size} records)`, () => { + const map = new Map(); + testData.forEach(record => map.set(record.id, record)); + }, 10); + results.push(mapSetResult); + + // Native Object storage + const objectSetResult = benchmark(`Object SET (${size} records)`, () => { + const obj = {}; + testData.forEach(record => obj[record.id] = record); // eslint-disable-line no-return-assign + }, 10); + results.push(objectSetResult); + + // Array storage + const arraySetResult = benchmark(`Array PUSH (${size} records)`, () => { + const arr = []; + testData.forEach(record => arr.push(record)); + }, 10); + results.push(arraySetResult); + }); + + return results; +} + +/** + * Benchmarks retrieval operations comparison + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkRetrievalComparison (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Prepare data structures + const haroStore = haro(testData); + const mapStore = new Map(); + const objectStore = {}; + const arrayStore = []; + + testData.forEach(record => { + mapStore.set(record.id, record); + objectStore[record.id] = record; + arrayStore.push(record); + }); + + // Haro retrieval + const haroGetResult = benchmark(`Haro GET (${size} records)`, () => { + const id = Math.floor(Math.random() * size); + haroStore.get(id); + }); + results.push(haroGetResult); + + // Map retrieval + const mapGetResult = benchmark(`Map GET (${size} records)`, () => { + const id = Math.floor(Math.random() * size); + mapStore.get(id); + }); + results.push(mapGetResult); + + // Object retrieval + const objectGetResult = benchmark(`Object GET (${size} records)`, () => { + const id = Math.floor(Math.random() * size); + objectStore[id]; // eslint-disable-line no-unused-expressions + }); + results.push(objectGetResult); + + // Array retrieval (by index) + const arrayGetResult = benchmark(`Array GET (${size} records)`, () => { + const index = Math.floor(Math.random() * size); + arrayStore[index]; // eslint-disable-line no-unused-expressions + }); + results.push(arrayGetResult); + + // Array find (by property) + const arrayFindResult = benchmark(`Array FIND (${size} records)`, () => { + const id = Math.floor(Math.random() * size); + arrayStore.find(record => record.id === id); + }); + results.push(arrayFindResult); + }); + + return results; +} + +/** + * Benchmarks query operations comparison + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkQueryComparison (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Prepare data structures + const haroStore = haro(testData, { index: ["category", "status"] }); + const arrayStore = [...testData]; + + // Haro indexed query + const haroQueryResult = benchmark(`Haro FIND indexed (${size} records)`, () => { + haroStore.find({ category: "A" }); + }); + results.push(haroQueryResult); + + // Haro filter query + const haroFilterResult = benchmark(`Haro FILTER (${size} records)`, () => { + haroStore.filter(record => record.category === "A"); + }); + results.push(haroFilterResult); + + // Array filter query + const arrayFilterResult = benchmark(`Array FILTER (${size} records)`, () => { + arrayStore.filter(record => record.category === "A"); + }); + results.push(arrayFilterResult); + + // Complex query comparison + const haroComplexResult = benchmark(`Haro COMPLEX query (${size} records)`, () => { + haroStore.filter(record => + record.category === "A" && + record.status === "active" && + record.priority === "high" + ); + }); + results.push(haroComplexResult); + + const arrayComplexResult = benchmark(`Array COMPLEX query (${size} records)`, () => { + arrayStore.filter(record => + record.category === "A" && + record.status === "active" && + record.priority === "high" + ); + }); + results.push(arrayComplexResult); + }); + + return results; +} + +/** + * Benchmarks deletion operations comparison + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkDeletionComparison (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Haro deletion + const haroDeleteResult = benchmark(`Haro DELETE (${size} records)`, () => { + const store = haro(testData); + const keys = Array.from(store.keys()); + for (let i = 0; i < Math.min(100, keys.length); i++) { + try { + store.del(keys[i]); + } catch (e) { // eslint-disable-line no-unused-vars + // Record might already be deleted + } + } + }, 10); + results.push(haroDeleteResult); + + // Map deletion + const mapDeleteResult = benchmark(`Map DELETE (${size} records)`, () => { + const map = new Map(); + testData.forEach(record => map.set(record.id, record)); + const keys = Array.from(map.keys()); + for (let i = 0; i < Math.min(100, keys.length); i++) { + map.delete(keys[i]); + } + }, 10); + results.push(mapDeleteResult); + + // Object deletion + const objectDeleteResult = benchmark(`Object DELETE (${size} records)`, () => { + const obj = {}; + testData.forEach(record => obj[record.id] = record); // eslint-disable-line no-return-assign + const keys = Object.keys(obj); + for (let i = 0; i < Math.min(100, keys.length); i++) { + delete obj[keys[i]]; + } + }, 10); + results.push(objectDeleteResult); + + // Array splice deletion + const arrayDeleteResult = benchmark(`Array SPLICE (${size} records)`, () => { + const arr = [...testData]; + for (let i = 0; i < Math.min(100, arr.length); i++) { + arr.splice(0, 1); + } + }, 10); + results.push(arrayDeleteResult); + }); + + return results; +} + +/** + * Benchmarks aggregation operations comparison + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkAggregationComparison (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Prepare data structures + const haroStore = haro(testData); + const arrayStore = [...testData]; + + // Haro map operation + const haroMapResult = benchmark(`Haro MAP (${size} records)`, () => { + haroStore.map(record => record.category); + }); + results.push(haroMapResult); + + // Array map operation + const arrayMapResult = benchmark(`Array MAP (${size} records)`, () => { + arrayStore.map(record => record.category); + }); + results.push(arrayMapResult); + + // Haro reduce operation + const haroReduceResult = benchmark(`Haro REDUCE (${size} records)`, () => { + haroStore.reduce((acc, record) => { + acc[record.category] = (acc[record.category] || 0) + 1; + + return acc; + }, {}); + }); + results.push(haroReduceResult); + + // Array reduce operation + const arrayReduceResult = benchmark(`Array REDUCE (${size} records)`, () => { + arrayStore.reduce((acc, record) => { + acc[record.category] = (acc[record.category] || 0) + 1; + + return acc; + }, {}); + }); + results.push(arrayReduceResult); + + // Haro forEach operation + const haroForEachResult = benchmark(`Haro FOREACH (${size} records)`, () => { + let count = 0; + haroStore.forEach(() => count++); + }); + results.push(haroForEachResult); + + // Array forEach operation + const arrayForEachResult = benchmark(`Array FOREACH (${size} records)`, () => { + let count = 0; + arrayStore.forEach(() => count++); + }); + results.push(arrayForEachResult); + }); + + return results; +} + +/** + * Benchmarks sorting operations comparison + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkSortingComparison (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Prepare data structures + const haroStore = haro(testData, { index: ["category", "score"] }); + const arrayStore = [...testData]; + + // Haro sort operation + const haroSortResult = benchmark(`Haro SORT (${size} records)`, () => { + haroStore.sort((a, b) => a.score - b.score); + }, 10); + results.push(haroSortResult); + + // Array sort operation + const arraySortResult = benchmark(`Array SORT (${size} records)`, () => { + [...arrayStore].sort((a, b) => a.score - b.score); + }, 10); + results.push(arraySortResult); + + // Haro sortBy operation (indexed) + const haroSortByResult = benchmark(`Haro SORTBY indexed (${size} records)`, () => { + haroStore.sortBy("score"); + }, 10); + results.push(haroSortByResult); + + // Complex sort comparison + const haroComplexSortResult = benchmark(`Haro COMPLEX sort (${size} records)`, () => { + haroStore.sort((a, b) => { + if (a.category !== b.category) { + return a.category.localeCompare(b.category); + } + + return b.score - a.score; + }); + }, 10); + results.push(haroComplexSortResult); + + const arrayComplexSortResult = benchmark(`Array COMPLEX sort (${size} records)`, () => { + [...arrayStore].sort((a, b) => { + if (a.category !== b.category) { + return a.category.localeCompare(b.category); + } + + return b.score - a.score; + }); + }, 10); + results.push(arrayComplexSortResult); + }); + + return results; +} + +/** + * Benchmarks memory efficiency comparison + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of memory comparison results + */ +function benchmarkMemoryComparison (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Measure memory usage for each data structure + const measurements = []; + + // Haro memory usage + const haroMemStart = process.memoryUsage().heapUsed; + const haroStore = haro(testData); // eslint-disable-line no-unused-vars + const haroMemEnd = process.memoryUsage().heapUsed; + measurements.push({ + name: `Haro memory (${size} records)`, + memoryUsed: (haroMemEnd - haroMemStart) / 1024 / 1024 // MB + }); + + // Map memory usage + const mapMemStart = process.memoryUsage().heapUsed; + const mapStore = new Map(); + testData.forEach(record => mapStore.set(record.id, record)); + const mapMemEnd = process.memoryUsage().heapUsed; + measurements.push({ + name: `Map memory (${size} records)`, + memoryUsed: (mapMemEnd - mapMemStart) / 1024 / 1024 // MB + }); + + // Object memory usage + const objMemStart = process.memoryUsage().heapUsed; + const objStore = {}; + testData.forEach(record => objStore[record.id] = record); // eslint-disable-line no-return-assign + const objMemEnd = process.memoryUsage().heapUsed; + measurements.push({ + name: `Object memory (${size} records)`, + memoryUsed: (objMemEnd - objMemStart) / 1024 / 1024 // MB + }); + + // Array memory usage + const arrMemStart = process.memoryUsage().heapUsed; + const arrStore = [...testData]; // eslint-disable-line no-unused-vars + const arrMemEnd = process.memoryUsage().heapUsed; + measurements.push({ + name: `Array memory (${size} records)`, + memoryUsed: (arrMemEnd - arrMemStart) / 1024 / 1024 // MB + }); + + results.push(...measurements); + }); + + return results; +} + +/** + * Benchmarks advanced features unique to Haro + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkAdvancedFeatures (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Haro advanced features + const haroAdvancedResult = benchmark(`Haro ADVANCED features (${size} records)`, () => { + const store = haro(testData, { + index: ["category", "status", "category|status"], + versioning: true + }); + + // Use advanced features + store.find({ category: "A", status: "active" }); + store.search(/^A/, "category"); + store.where({ category: ["A", "B"] }); + store.sortBy("category"); + store.limit(10, 20); + + return store; + }, 10); + results.push(haroAdvancedResult); + + // Simulate similar operations with native structures + const nativeAdvancedResult = benchmark(`Native ADVANCED simulation (${size} records)`, () => { + const store = [...testData]; + + // Category index simulation + const categoryIndex = new Map(); + store.forEach(record => { + if (!categoryIndex.has(record.category)) { + categoryIndex.set(record.category, []); + } + categoryIndex.get(record.category).push(record); + }); + + // Find simulation + const found = store.filter(record => record.category === "A" && record.status === "active"); + + // Search simulation + const searched = store.filter(record => (/^A/).test(record.category)); + + // Where simulation + const where = store.filter(record => ["A", "B"].includes(record.category)); + + // Sort simulation + const sorted = [...store].sort((a, b) => a.category.localeCompare(b.category)); + + // Limit simulation + const limited = sorted.slice(10, 30); + + return { found, searched, where, sorted, limited }; + }, 10); + results.push(nativeAdvancedResult); + }); + + return results; +} + +/** + * Prints comparison results in a formatted table + * @param {Array} results - Array of benchmark results + * @param {string} title - Title for the results section + */ +function printResults (results, title) { + console.log(`\n=== ${title} ===\n`); + + console.log("Operation".padEnd(40) + "Iterations".padEnd(12) + "Total Time (ms)".padEnd(18) + "Avg Time (ms)".padEnd(16) + "Ops/Second"); + console.log("-".repeat(98)); + + results.forEach(result => { + const name = result.name.padEnd(40); + const iterations = result.iterations.toString().padEnd(12); + const totalTime = result.totalTime.toFixed(2).padEnd(18); + const avgTime = result.avgTime.toFixed(4).padEnd(16); + const opsPerSecond = result.opsPerSecond.toLocaleString(); + + console.log(name + iterations + totalTime + avgTime + opsPerSecond); + }); + + console.log("\n"); +} + +/** + * Prints memory comparison results + * @param {Array} results - Array of memory measurements + */ +function printMemoryResults (results) { + console.log("\n=== MEMORY USAGE COMPARISON ===\n"); + + console.log("Data Structure".padEnd(40) + "Memory Used (MB)"); + console.log("-".repeat(60)); + + results.forEach(result => { + const name = result.name.padEnd(40); + const memoryUsed = result.memoryUsed.toFixed(2); + + console.log(name + memoryUsed); + }); + + console.log("\n"); +} + +/** + * Main function to run all comparison benchmarks + */ +function runComparisonBenchmarks () { + console.log("⚡ Running Haro vs Native Structures Comparison...\n"); + + const dataSizes = [1000, 10000, 50000]; + + console.log("Testing storage operations..."); + const storageResults = benchmarkStorageComparison(dataSizes); + printResults(storageResults, "STORAGE OPERATIONS COMPARISON"); + + console.log("Testing retrieval operations..."); + const retrievalResults = benchmarkRetrievalComparison(dataSizes); + printResults(retrievalResults, "RETRIEVAL OPERATIONS COMPARISON"); + + console.log("Testing query operations..."); + const queryResults = benchmarkQueryComparison(dataSizes); + printResults(queryResults, "QUERY OPERATIONS COMPARISON"); + + console.log("Testing deletion operations..."); + const deletionResults = benchmarkDeletionComparison(dataSizes); + printResults(deletionResults, "DELETION OPERATIONS COMPARISON"); + + console.log("Testing aggregation operations..."); + const aggregationResults = benchmarkAggregationComparison(dataSizes); + printResults(aggregationResults, "AGGREGATION OPERATIONS COMPARISON"); + + console.log("Testing sorting operations..."); + const sortingResults = benchmarkSortingComparison(dataSizes); + printResults(sortingResults, "SORTING OPERATIONS COMPARISON"); + + console.log("Testing advanced features..."); + const advancedResults = benchmarkAdvancedFeatures(dataSizes); + printResults(advancedResults, "ADVANCED FEATURES COMPARISON"); + + console.log("Testing memory usage..."); + const memoryResults = benchmarkMemoryComparison(dataSizes); + printMemoryResults(memoryResults); + + const allResults = [ + ...storageResults, + ...retrievalResults, + ...queryResults, + ...deletionResults, + ...aggregationResults, + ...sortingResults, + ...advancedResults + ]; + + return { allResults, memoryResults }; +} + +// Run benchmarks if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runComparisonBenchmarks(); +} + +export { runComparisonBenchmarks }; diff --git a/benchmarks/immutable-comparison.js b/benchmarks/immutable-comparison.js new file mode 100644 index 00000000..602d54de --- /dev/null +++ b/benchmarks/immutable-comparison.js @@ -0,0 +1,631 @@ +import { performance } from "node:perf_hooks"; +import { haro } from "../dist/haro.js"; + +/** + * Generates test data for immutable vs mutable comparison benchmarking + * @param {number} size - Number of records to generate + * @returns {Array} Array of test records + */ +function generateComparisonTestData (size) { + const data = []; + for (let i = 0; i < size; i++) { + data.push({ + id: i, + name: `User ${i}`, + email: `user${i}@example.com`, + age: Math.floor(Math.random() * 50) + 18, + department: `Dept ${i % 10}`, + active: Math.random() > 0.2, + tags: [`tag${i % 15}`, `category${i % 8}`, `type${i % 5}`], + score: Math.random() * 100, + metadata: { + created: new Date(), + level: Math.floor(Math.random() * 10), + preferences: { + theme: i % 2 === 0 ? "dark" : "light", + notifications: Math.random() > 0.5, + language: ["en", "es", "fr"][i % 3] + } + }, + history: Array.from({ length: Math.min(i % 10 + 1, 5) }, (_, j) => ({ + action: `action_${j}`, + timestamp: new Date(Date.now() - j * 86400000), + value: Math.random() * 1000 + })) + }); + } + + return data; +} + +/** + * Runs a benchmark test and returns timing information + * @param {string} name - Name of the test + * @param {Function} fn - Function to benchmark + * @param {number} iterations - Number of iterations to run + * @returns {Object} Benchmark results + */ +function benchmark (name, fn, iterations = 100) { + const start = performance.now(); + for (let i = 0; i < iterations; i++) { + fn(); + } + const end = performance.now(); + const total = end - start; + const avgTime = total / iterations; + + return { + name, + iterations, + totalTime: total, + avgTime, + opsPerSecond: Math.floor(1000 / avgTime) + }; +} + +/** + * Benchmarks store creation and initial data loading + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkStoreCreation (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateComparisonTestData(size); + + // Mutable store creation + const mutableCreationResult = benchmark(`Store creation MUTABLE (${size} records)`, () => { + return haro(testData, { immutable: false, index: ["department", "active", "tags"] }); + }, 10); + results.push(mutableCreationResult); + + // Immutable store creation + const immutableCreationResult = benchmark(`Store creation IMMUTABLE (${size} records)`, () => { + return haro(testData, { immutable: true, index: ["department", "active", "tags"] }); + }, 10); + results.push(immutableCreationResult); + + // Performance comparison + const performanceRatio = (mutableCreationResult.opsPerSecond / immutableCreationResult.opsPerSecond).toFixed(2); + results.push({ + name: `Creation performance ratio (${size} records)`, + iterations: 1, + totalTime: 0, + avgTime: 0, + opsPerSecond: 0, + mutableOps: mutableCreationResult.opsPerSecond, + immutableOps: immutableCreationResult.opsPerSecond, + ratio: `${performanceRatio}x faster (mutable)`, + recommendation: parseFloat(performanceRatio) > 1.5 ? "Use mutable for creation-heavy workloads" : "Performance difference minimal" + }); + }); + + return results; +} + +/** + * Benchmarks basic CRUD operations in both modes + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkCrudOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateComparisonTestData(size); + + // Create stores + const mutableStore = haro(testData, { immutable: false, index: ["department", "active"] }); + const immutableStore = haro(testData, { immutable: true, index: ["department", "active"] }); + + // GET operations + const mutableGetResult = benchmark(`GET operation MUTABLE (${size} records)`, () => { + const randomId = Math.floor(Math.random() * size).toString(); + + return mutableStore.get(randomId); + }); + results.push(mutableGetResult); + + const immutableGetResult = benchmark(`GET operation IMMUTABLE (${size} records)`, () => { + const randomId = Math.floor(Math.random() * size).toString(); + + return immutableStore.get(randomId); + }); + results.push(immutableGetResult); + + // SET operations + const mutableSetResult = benchmark(`SET operation MUTABLE (${size} records)`, () => { + const randomId = Math.floor(Math.random() * size).toString(); + + return mutableStore.set(randomId, { ...testData[0], updated: Date.now() }); + }); + results.push(mutableSetResult); + + const immutableSetResult = benchmark(`SET operation IMMUTABLE (${size} records)`, () => { + const randomId = Math.floor(Math.random() * size).toString(); + + return immutableStore.set(randomId, { ...testData[0], updated: Date.now() }); + }); + results.push(immutableSetResult); + + // DELETE operations (using a subset to avoid depleting data) + const deleteCount = Math.min(10, size); + const mutableDeleteResult = benchmark(`DELETE operation MUTABLE (${deleteCount} deletes)`, () => { + const randomId = Math.floor(Math.random() * (size - deleteCount)).toString(); + try { + mutableStore.delete(randomId); + } catch (e) { // eslint-disable-line no-unused-vars + // Record might not exist + } + }, deleteCount); + results.push(mutableDeleteResult); + + const immutableDeleteResult = benchmark(`DELETE operation IMMUTABLE (${deleteCount} deletes)`, () => { + const randomId = Math.floor(Math.random() * (size - deleteCount)).toString(); + try { + immutableStore.delete(randomId); + } catch (e) { // eslint-disable-line no-unused-vars + // Record might not exist + } + }, deleteCount); + results.push(immutableDeleteResult); + }); + + return results; +} + +/** + * Benchmarks query operations in both modes + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkQueryOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateComparisonTestData(size); + + // Create stores with extensive indexing + const mutableStore = haro(testData, { + immutable: false, + index: ["department", "active", "tags", "age", "department|active"] + }); + const immutableStore = haro(testData, { + immutable: true, + index: ["department", "active", "tags", "age", "department|active"] + }); + + // FIND operations + const mutableFindResult = benchmark(`FIND operation MUTABLE (${size} records)`, () => { + return mutableStore.find({ department: "Dept 0" }); + }); + results.push(mutableFindResult); + + const immutableFindResult = benchmark(`FIND operation IMMUTABLE (${size} records)`, () => { + return immutableStore.find({ department: "Dept 0" }); + }); + results.push(immutableFindResult); + + // FILTER operations + const mutableFilterResult = benchmark(`FILTER operation MUTABLE (${size} records)`, () => { + return mutableStore.filter(record => record.age > 30); + }); + results.push(mutableFilterResult); + + const immutableFilterResult = benchmark(`FILTER operation IMMUTABLE (${size} records)`, () => { + return immutableStore.filter(record => record.age > 30); + }); + results.push(immutableFilterResult); + + // WHERE operations + const mutableWhereResult = benchmark(`WHERE operation MUTABLE (${size} records)`, () => { + return mutableStore.where({ + department: ["Dept 0", "Dept 1"], + active: true + }); + }); + results.push(mutableWhereResult); + + const immutableWhereResult = benchmark(`WHERE operation IMMUTABLE (${size} records)`, () => { + return immutableStore.where({ + department: ["Dept 0", "Dept 1"], + active: true + }); + }); + results.push(immutableWhereResult); + + // SEARCH operations + const mutableSearchResult = benchmark(`SEARCH operation MUTABLE (${size} records)`, () => { + return mutableStore.search("tag0"); + }); + results.push(mutableSearchResult); + + const immutableSearchResult = benchmark(`SEARCH operation IMMUTABLE (${size} records)`, () => { + return immutableStore.search("tag0"); + }); + results.push(immutableSearchResult); + }); + + return results; +} + +/** + * Benchmarks transformation operations in both modes + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkTransformationOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateComparisonTestData(size); + + // Create stores + const mutableStore = haro(testData, { immutable: false }); + const immutableStore = haro(testData, { immutable: true }); + + // MAP operations + const mutableMapResult = benchmark(`MAP operation MUTABLE (${size} records)`, () => { + return mutableStore.map(record => ({ + id: record.id, + name: record.name, + summary: `${record.name} - ${record.department}` + })); + }); + results.push(mutableMapResult); + + const immutableMapResult = benchmark(`MAP operation IMMUTABLE (${size} records)`, () => { + return immutableStore.map(record => ({ + id: record.id, + name: record.name, + summary: `${record.name} - ${record.department}` + })); + }); + results.push(immutableMapResult); + + // REDUCE operations + const mutableReduceResult = benchmark(`REDUCE operation MUTABLE (${size} records)`, () => { + return mutableStore.reduce((acc, record) => { + acc[record.department] = (acc[record.department] || 0) + 1; + + return acc; + }, {}); + }); + results.push(mutableReduceResult); + + const immutableReduceResult = benchmark(`REDUCE operation IMMUTABLE (${size} records)`, () => { + return immutableStore.reduce((acc, record) => { + acc[record.department] = (acc[record.department] || 0) + 1; + + return acc; + }, {}); + }); + results.push(immutableReduceResult); + + // SORT operations + const mutableSortResult = benchmark(`SORT operation MUTABLE (${size} records)`, () => { + return mutableStore.sort((a, b) => a.score - b.score); + }, 10); + results.push(mutableSortResult); + + const immutableSortResult = benchmark(`SORT operation IMMUTABLE (${size} records)`, () => { + return immutableStore.sort((a, b) => a.score - b.score); + }, 10); + results.push(immutableSortResult); + + // forEach operations + const mutableForEachResult = benchmark(`forEach operation MUTABLE (${size} records)`, () => { + let count = 0; + mutableStore.forEach(() => { count++; }); + + return count; + }); + results.push(mutableForEachResult); + + const immutableForEachResult = benchmark(`forEach operation IMMUTABLE (${size} records)`, () => { + let count = 0; + immutableStore.forEach(() => { count++; }); + + return count; + }); + results.push(immutableForEachResult); + }); + + return results; +} + +/** + * Benchmarks memory usage patterns between modes + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkMemoryUsage (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateComparisonTestData(size); + + // Test memory usage for mutable store + if (global.gc) { + global.gc(); + } + const memBefore = process.memoryUsage().heapUsed; + + const mutableStore = haro(testData, { immutable: false, index: ["department", "active"] }); + + if (global.gc) { + global.gc(); + } + const memAfterMutable = process.memoryUsage().heapUsed; + + // Test memory usage for immutable store + const immutableStore = haro(testData, { immutable: true, index: ["department", "active"] }); + + if (global.gc) { + global.gc(); + } + const memAfterImmutable = process.memoryUsage().heapUsed; + + // Test memory usage during operations + const operationsStart = performance.now(); + + // Perform some operations on mutable store + for (let i = 0; i < Math.min(100, size); i++) { + const result = mutableStore.find({ department: `Dept ${i % 10}` }); // eslint-disable-line no-unused-vars + mutableStore.set(`temp_${i}`, { ...testData[0], temp: true }); + } + + if (global.gc) { + global.gc(); + } + const memAfterMutableOps = process.memoryUsage().heapUsed; + + // Perform same operations on immutable store + for (let i = 0; i < Math.min(100, size); i++) { + const result = immutableStore.find({ department: `Dept ${i % 10}` }); // eslint-disable-line no-unused-vars + immutableStore.set(`temp_${i}`, { ...testData[0], temp: true }); + } + + if (global.gc) { + global.gc(); + } + const memAfterImmutableOps = process.memoryUsage().heapUsed; + + const operationsEnd = performance.now(); + + results.push({ + name: `Memory usage comparison (${size} records)`, + iterations: 1, + totalTime: operationsEnd - operationsStart, + avgTime: 0, + opsPerSecond: Math.floor(200 / ((operationsEnd - operationsStart) / 1000)), // 200 ops total + mutableStoreMemory: (memAfterMutable - memBefore) / 1024 / 1024, // MB + immutableStoreMemory: (memAfterImmutable - memAfterMutable) / 1024 / 1024, // MB + mutableOpsMemory: (memAfterMutableOps - memAfterMutable) / 1024 / 1024, // MB + immutableOpsMemory: (memAfterImmutableOps - memAfterMutableOps) / 1024 / 1024, // MB + totalMutableMemory: (memAfterMutableOps - memBefore) / 1024 / 1024, // MB + totalImmutableMemory: (memAfterImmutableOps - memAfterMutable) / 1024 / 1024 // MB + }); + }); + + return results; +} + +/** + * Benchmarks data safety and mutation detection + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkDataSafety (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateComparisonTestData(Math.min(size, 100)); // Limit for safety tests + + // Create stores + const mutableStore = haro(testData, { immutable: false }); + const immutableStore = haro(testData, { immutable: true }); + + // Test mutation safety + const mutableRecord = mutableStore.get(0); + const immutableRecord = immutableStore.get(0); + + // Attempt to mutate records + const mutationStart = performance.now(); + + try { + // This should work for mutable + mutableRecord.name = "MUTATED"; + mutableRecord.tags.push("new-tag"); + } catch (e) { // eslint-disable-line no-unused-vars + // Mutation failed + } + + try { + // This should fail for immutable + immutableRecord.name = "MUTATED"; + immutableRecord.tags.push("new-tag"); + } catch (e) { // eslint-disable-line no-unused-vars + // Expected failure for immutable + } + + const mutationEnd = performance.now(); + + // Check if mutations actually occurred + const mutableRecordAfter = mutableStore.get(0); + const immutableRecordAfter = immutableStore.get(0); + + results.push({ + name: `Data safety analysis (${testData.length} records)`, + iterations: 1, + totalTime: mutationEnd - mutationStart, + avgTime: 0, + opsPerSecond: 0, + mutableMutated: mutableRecordAfter.name === "MUTATED", + immutableMutated: immutableRecordAfter.name === "MUTATED", + mutableProtected: mutableRecordAfter.name !== "MUTATED", + immutableProtected: immutableRecordAfter.name !== "MUTATED", + recommendation: "Use immutable mode for data safety in multi-consumer environments" + }); + }); + + return results; +} + +/** + * Generates performance recommendations based on benchmark results + * @param {Array} results - All benchmark results + * @returns {Object} Performance recommendations + */ +function generatePerformanceRecommendations (results) { + const recommendations = { + general: [], + mutableAdvantages: [], + immutableAdvantages: [], + useCase: {} + }; + + // Analyze results to generate recommendations + const mutableOps = results.filter(r => r.name.includes("MUTABLE")).map(r => r.opsPerSecond); + const immutableOps = results.filter(r => r.name.includes("IMMUTABLE")).map(r => r.opsPerSecond); + + const avgMutablePerf = mutableOps.reduce((a, b) => a + b, 0) / mutableOps.length; + const avgImmutablePerf = immutableOps.reduce((a, b) => a + b, 0) / immutableOps.length; + + if (avgMutablePerf > avgImmutablePerf * 1.2) { + recommendations.general.push("Mutable mode shows significant performance advantages"); + recommendations.mutableAdvantages.push("Faster overall operations"); + } else if (avgImmutablePerf > avgMutablePerf * 1.2) { + recommendations.general.push("Immutable mode shows competitive performance"); + recommendations.immutableAdvantages.push("Good performance with data safety"); + } else { + recommendations.general.push("Performance difference is minimal between modes"); + } + + // Use case recommendations + recommendations.useCase = { + "High-frequency writes": "Consider mutable mode for better write performance", + "Data safety critical": "Use immutable mode to prevent accidental mutations", + "Multi-consumer reads": "Immutable mode provides safer concurrent access", + "Memory constrained": "Mutable mode may use less memory", + "Development/debugging": "Immutable mode helps catch mutation bugs early" + }; + + return recommendations; +} + +/** + * Prints formatted benchmark results with detailed analysis + * @param {Array} results - Array of benchmark results + */ +function printResults (results) { + console.log("\n" + "=".repeat(80)); + console.log("IMMUTABLE vs MUTABLE COMPARISON RESULTS"); + console.log("=".repeat(80)); + + // Group results by operation type + const groupedResults = {}; + results.forEach(result => { + const operation = result.name.split(" ").slice(-2, -1)[0] || "Analysis"; + if (!groupedResults[operation]) { + groupedResults[operation] = []; + } + groupedResults[operation].push(result); + }); + + Object.keys(groupedResults).forEach(operation => { + console.log(`\n${operation.toUpperCase()} OPERATIONS:`); + console.log("-".repeat(50)); + + groupedResults[operation].forEach(result => { + if (result.opsPerSecond > 0) { + const opsIndicator = result.opsPerSecond > 1000 ? "✅" : + result.opsPerSecond > 100 ? "🟡" : + result.opsPerSecond > 10 ? "🟠" : "🔴"; + + console.log(`${opsIndicator} ${result.name}`); + console.log(` ${result.opsPerSecond.toLocaleString()} ops/sec | ${result.totalTime.toFixed(2)}ms total`); + } else { + console.log(`📊 ${result.name}`); + } + + // Special formatting for analysis results + if (result.ratio) { + console.log(` Performance ratio: ${result.ratio}`); + console.log(` Recommendation: ${result.recommendation}`); + } + + if (result.mutableStoreMemory !== undefined) { + console.log(` Memory - Mutable store: ${result.mutableStoreMemory.toFixed(2)}MB | Immutable store: ${result.immutableStoreMemory.toFixed(2)}MB`); + console.log(` Memory - Mutable ops: +${result.mutableOpsMemory.toFixed(2)}MB | Immutable ops: +${result.immutableOpsMemory.toFixed(2)}MB`); + } + + if (result.mutableMutated !== undefined) { + console.log(` Mutable protection: ${result.mutableProtected ? "❌" : "✅"} | Immutable protection: ${result.immutableProtected ? "✅" : "❌"}`); + console.log(` ${result.recommendation}`); + } + + console.log(""); + }); + }); + + // Generate and display recommendations + const recommendations = generatePerformanceRecommendations(results); + console.log("\n" + "=".repeat(80)); + console.log("PERFORMANCE RECOMMENDATIONS"); + console.log("=".repeat(80)); + + console.log("\nGeneral Findings:"); + recommendations.general.forEach(rec => console.log(`• ${rec}`)); + + console.log("\nUse Case Recommendations:"); + Object.keys(recommendations.useCase).forEach(useCase => { + console.log(`• ${useCase}: ${recommendations.useCase[useCase]}`); + }); + + console.log(""); +} + +/** + * Runs all immutable vs mutable comparison benchmarks + * @returns {Array} Array of all benchmark results + */ +function runImmutableComparisonBenchmarks () { + console.log("Starting Immutable vs Mutable Comparison Benchmarks...\n"); + + const dataSizes = [100, 1000, 5000]; + let allResults = []; + + console.log("Testing store creation..."); + allResults.push(...benchmarkStoreCreation(dataSizes)); + + console.log("Testing CRUD operations..."); + allResults.push(...benchmarkCrudOperations(dataSizes)); + + console.log("Testing query operations..."); + allResults.push(...benchmarkQueryOperations(dataSizes)); + + console.log("Testing transformation operations..."); + allResults.push(...benchmarkTransformationOperations(dataSizes)); + + console.log("Testing memory usage..."); + allResults.push(...benchmarkMemoryUsage([1000, 5000])); // Smaller sizes for memory tests + + console.log("Testing data safety..."); + allResults.push(...benchmarkDataSafety([100])); // Small size for safety tests + + printResults(allResults); + + console.log("Immutable vs Mutable Comparison Benchmarks completed.\n"); + + return allResults; +} + +// Export for use in main benchmark runner +export { runImmutableComparisonBenchmarks }; + +// Run standalone if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runImmutableComparisonBenchmarks(); +} diff --git a/benchmarks/index-operations.js b/benchmarks/index-operations.js new file mode 100644 index 00000000..e4acf074 --- /dev/null +++ b/benchmarks/index-operations.js @@ -0,0 +1,434 @@ +import { performance } from "node:perf_hooks"; +import { haro } from "../dist/haro.js"; + +/** + * Generates test data with various indexable fields + * @param {number} size - Number of records to generate + * @returns {Array} Array of test records optimized for indexing + */ +function generateIndexTestData (size) { + const data = []; + const categories = ["A", "B", "C", "D", "E"]; + const statuses = ["active", "inactive", "pending", "suspended"]; + const priorities = ["low", "medium", "high", "urgent"]; + const regions = ["north", "south", "east", "west"]; + + for (let i = 0; i < size; i++) { + data.push({ + id: i, + category: categories[i % categories.length], + status: statuses[i % statuses.length], + priority: priorities[i % priorities.length], + region: regions[i % regions.length], + userId: Math.floor(i / 10), // Creates groups of 10 + projectId: Math.floor(i / 100), // Creates groups of 100 + timestamp: new Date(2024, 0, 1, 0, 0, 0, i * 1000), + score: Math.floor(Math.random() * 1000), + tags: [ + `tag${i % 20}`, + `category${i % 10}`, + `type${i % 5}` + ], + metadata: { + level: Math.floor(Math.random() * 10), + department: `Dept${i % 15}`, + location: `Location${i % 25}` + }, + flags: { + isPublic: Math.random() > 0.5, + isVerified: Math.random() > 0.3, + isUrgent: Math.random() > 0.9 + } + }); + } + + return data; +} + +/** + * Runs a benchmark test and returns timing information + * @param {string} name - Name of the test + * @param {Function} fn - Function to benchmark + * @param {number} iterations - Number of iterations to run + * @returns {Object} Benchmark results + */ +function benchmark (name, fn, iterations = 100) { + const start = performance.now(); + for (let i = 0; i < iterations; i++) { + fn(); + } + const end = performance.now(); + const total = end - start; + const avgTime = total / iterations; + + return { + name, + iterations, + totalTime: total, + avgTime, + opsPerSecond: Math.floor(1000 / avgTime) + }; +} + +/** + * Benchmarks single field index creation and reindexing + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkSingleIndexOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Initial index creation during construction + const initialIndexResult = benchmark(`CREATE initial indexes (${size} records)`, () => { + const store = haro(testData, { + index: ["category", "status", "priority", "region", "userId"] + }); + + return store; + }, 10); + results.push(initialIndexResult); + + // Reindex single field + const store = haro(testData, { index: ["category"] }); + const reindexSingleResult = benchmark(`REINDEX single field (${size} records)`, () => { + store.reindex("status"); + }, 10); + results.push(reindexSingleResult); + + // Reindex all fields + const reindexAllResult = benchmark(`REINDEX all fields (${size} records)`, () => { + store.reindex(); + }, 5); + results.push(reindexAllResult); + }); + + return results; +} + +/** + * Benchmarks composite index operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkCompositeIndexOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Create composite indexes + const compositeIndexResult = benchmark(`CREATE composite indexes (${size} records)`, () => { + const store = haro(testData, { + index: [ + "category|status", + "region|priority", + "userId|projectId", + "category|status|priority", + "region|category|status" + ] + }); + + return store; + }, 5); + results.push(compositeIndexResult); + + // Query composite indexes + const store = haro(testData, { + index: ["category|status", "region|priority", "userId|projectId"] + }); + + const queryCompositeResult = benchmark(`QUERY composite index (${size} records)`, () => { + store.find({ category: "A", status: "active" }); + }); + results.push(queryCompositeResult); + + const queryTripleCompositeResult = benchmark(`QUERY triple composite (${size} records)`, () => { + store.find({ category: "A", status: "active", priority: "high" }); + }); + results.push(queryTripleCompositeResult); + }); + + return results; +} + +/** + * Benchmarks array field indexing + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkArrayIndexOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Create array field indexes + const arrayIndexResult = benchmark(`CREATE array indexes (${size} records)`, () => { + const store = haro(testData, { + index: ["tags", "tags|category", "tags|status"] + }); + + return store; + }, 5); + results.push(arrayIndexResult); + + // Query array indexes + const store = haro(testData, { index: ["tags"] }); + const queryArrayResult = benchmark(`QUERY array index (${size} records)`, () => { + store.find({ tags: "tag1" }); + }); + results.push(queryArrayResult); + + // Search array indexes + const searchArrayResult = benchmark(`SEARCH array index (${size} records)`, () => { + store.search("tag1", "tags"); + }); + results.push(searchArrayResult); + }); + + return results; +} + +/** + * Benchmarks nested field indexing + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkNestedIndexOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Create nested field indexes (simulated with dot notation) + const nestedIndexResult = benchmark(`CREATE nested indexes (${size} records)`, () => { + const store = haro(testData, { + index: ["metadata.level", "metadata.department", "flags.isPublic"] + }); + + return store; + }, 5); + results.push(nestedIndexResult); + }); + + return results; +} + +/** + * Benchmarks index performance under different data modification patterns + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkIndexModificationOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + const store = haro(testData, { + index: ["category", "status", "priority", "category|status", "userId"] + }); + + // Benchmark SET operations with existing indexes + const setWithIndexResult = benchmark(`SET with indexes (${size} records)`, () => { + const randomId = Math.floor(Math.random() * size); + store.set(randomId, { + ...testData[randomId], + category: "Z", + status: "updated", + timestamp: new Date() + }); + }, 100); + results.push(setWithIndexResult); + + // Benchmark DELETE operations with existing indexes + const deleteWithIndexResult = benchmark(`DELETE with indexes (${size} records)`, () => { + const keys = Array.from(store.keys()); + if (keys.length > 0) { + const randomKey = keys[Math.floor(Math.random() * keys.length)]; + try { + store.del(randomKey); + } catch (e) { // eslint-disable-line no-unused-vars + // Record might already be deleted + } + } + }, 50); + results.push(deleteWithIndexResult); + + // Benchmark BATCH operations with existing indexes + const batchWithIndexResult = benchmark(`BATCH with indexes (${size} records)`, () => { + const batchData = testData.slice(0, 10).map(item => ({ + ...item, + category: "BATCH", + status: "batch_updated" + })); + store.batch(batchData, "set"); + }, 10); + results.push(batchWithIndexResult); + }); + + return results; +} + +/** + * Benchmarks index memory usage and export/import operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkIndexMemoryOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + const store = haro(testData, { + index: ["category", "status", "priority", "region", "userId", "category|status", "region|priority"] + }); + + // Benchmark index dump operations + const dumpIndexResult = benchmark(`DUMP indexes (${size} records)`, () => { + store.dump("indexes"); + }, 10); + results.push(dumpIndexResult); + + // Benchmark index override operations + const indexData = store.dump("indexes"); + const overrideIndexResult = benchmark(`OVERRIDE indexes (${size} records)`, () => { + const newStore = haro(); + newStore.override(indexData, "indexes"); + }, 10); + results.push(overrideIndexResult); + + // Benchmark index size measurement + const indexSizeResult = benchmark(`INDEX size check (${size} records)`, () => { + const indexes = store.indexes; + let totalSize = 0; + indexes.forEach(index => { + index.forEach(set => { + totalSize += set.size; + }); + }); + + return totalSize; + }, 100); + results.push(indexSizeResult); + }); + + return results; +} + +/** + * Benchmarks index performance comparison with and without indexes + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkIndexComparison (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Store without indexes + const storeNoIndex = haro(testData); + const filterNoIndexResult = benchmark(`FILTER no index (${size} records)`, () => { + storeNoIndex.filter(record => record.category === "A"); + }, 10); + results.push(filterNoIndexResult); + + // Store with indexes + const storeWithIndex = haro(testData, { index: ["category"] }); + const findWithIndexResult = benchmark(`FIND with index (${size} records)`, () => { + storeWithIndex.find({ category: "A" }); + }, 100); + results.push(findWithIndexResult); + + // Complex query without indexes + const complexFilterResult = benchmark(`COMPLEX filter no index (${size} records)`, () => { + storeNoIndex.filter(record => + record.category === "A" && + record.status === "active" && + record.priority === "high" + ); + }, 10); + results.push(complexFilterResult); + + // Complex query with indexes + const storeComplexIndex = haro(testData, { index: ["category", "status", "priority", "category|status|priority"] }); + const complexFindResult = benchmark(`COMPLEX find with index (${size} records)`, () => { + storeComplexIndex.find({ + category: "A", + status: "active", + priority: "high" + }); + }, 100); + results.push(complexFindResult); + }); + + return results; +} + +/** + * Prints benchmark results in a formatted table + * @param {Array} results - Array of benchmark results + */ +function printResults (results) { + console.log("\n=== INDEX OPERATIONS BENCHMARK RESULTS ===\n"); + + console.log("Operation".padEnd(40) + "Iterations".padEnd(12) + "Total Time (ms)".padEnd(18) + "Avg Time (ms)".padEnd(16) + "Ops/Second"); + console.log("-".repeat(98)); + + results.forEach(result => { + const name = result.name.padEnd(40); + const iterations = result.iterations.toString().padEnd(12); + const totalTime = result.totalTime.toFixed(2).padEnd(18); + const avgTime = result.avgTime.toFixed(4).padEnd(16); + const opsPerSecond = result.opsPerSecond.toLocaleString(); + + console.log(name + iterations + totalTime + avgTime + opsPerSecond); + }); + + console.log("\n"); +} + +/** + * Main function to run all index operations benchmarks + */ +function runIndexOperationsBenchmarks () { + console.log("📊 Running Index Operations Benchmarks...\n"); + + const dataSizes = [1000, 10000, 50000]; + const allResults = []; + + console.log("Testing single index operations..."); + allResults.push(...benchmarkSingleIndexOperations(dataSizes)); + + console.log("Testing composite index operations..."); + allResults.push(...benchmarkCompositeIndexOperations(dataSizes)); + + console.log("Testing array index operations..."); + allResults.push(...benchmarkArrayIndexOperations(dataSizes)); + + console.log("Testing nested index operations..."); + allResults.push(...benchmarkNestedIndexOperations(dataSizes)); + + console.log("Testing index modification operations..."); + allResults.push(...benchmarkIndexModificationOperations(dataSizes)); + + console.log("Testing index memory operations..."); + allResults.push(...benchmarkIndexMemoryOperations(dataSizes)); + + console.log("Testing index comparison..."); + allResults.push(...benchmarkIndexComparison(dataSizes)); + + printResults(allResults); + + return allResults; +} + +// Run benchmarks if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runIndexOperationsBenchmarks(); +} + +export { runIndexOperationsBenchmarks, generateIndexTestData }; diff --git a/benchmarks/index.js b/benchmarks/index.js new file mode 100644 index 00000000..064bf35a --- /dev/null +++ b/benchmarks/index.js @@ -0,0 +1,538 @@ +import { runBasicOperationsBenchmarks } from "./basic-operations.js"; +import { runSearchFilterBenchmarks } from "./search-filter.js"; +import { runIndexOperationsBenchmarks } from "./index-operations.js"; +import { runMemoryBenchmarks } from "./memory-usage.js"; +import { runComparisonBenchmarks } from "./comparison.js"; +import { runUtilityOperationsBenchmarks } from "./utility-operations.js"; +import { runPaginationBenchmarks } from "./pagination.js"; +import { runPersistenceBenchmarks } from "./persistence.js"; +import { runImmutableComparisonBenchmarks } from "./immutable-comparison.js"; + +/** + * Formats duration in milliseconds to human-readable format + * @param {number} ms - Duration in milliseconds + * @returns {string} Formatted duration string + */ +function formatDuration (ms) { + if (ms < 1000) { + return `${ms.toFixed(0)}ms`; + } else if (ms < 60000) { + return `${(ms / 1000).toFixed(1)}s`; + } else { + return `${(ms / 60000).toFixed(1)}m`; + } +} + +/** + * Generates a summary report of all benchmark results + * @param {Object} results - All benchmark results + * @returns {Object} Summary report + */ +function generateSummaryReport (results) { + const { basicOps, searchFilter, indexOps, memory, comparison, utilities, pagination, persistence, immutableComparison } = results; + + const summary = { + totalTests: 0, + totalTime: 0, + categories: {}, + performance: { + fastest: { name: "", opsPerSecond: 0 }, + slowest: { name: "", opsPerSecond: Infinity }, + mostMemoryEfficient: { name: "", memoryUsed: Infinity }, + leastMemoryEfficient: { name: "", memoryUsed: 0 } + }, + recommendations: [] + }; + + // Process basic operations + if (basicOps && basicOps.length > 0) { + summary.categories.basicOperations = { + testCount: basicOps.length, + totalTime: basicOps.reduce((sum, test) => sum + test.totalTime, 0), + avgOpsPerSecond: basicOps.reduce((sum, test) => sum + test.opsPerSecond, 0) / basicOps.length + }; + + // Find fastest and slowest operations + basicOps.forEach(test => { + if (test.opsPerSecond > summary.performance.fastest.opsPerSecond) { + summary.performance.fastest = { name: test.name, opsPerSecond: test.opsPerSecond }; + } + if (test.opsPerSecond < summary.performance.slowest.opsPerSecond) { + summary.performance.slowest = { name: test.name, opsPerSecond: test.opsPerSecond }; + } + }); + } + + // Process search and filter operations + if (searchFilter && searchFilter.length > 0) { + summary.categories.searchFilter = { + testCount: searchFilter.length, + totalTime: searchFilter.reduce((sum, test) => sum + test.totalTime, 0), + avgOpsPerSecond: searchFilter.reduce((sum, test) => sum + test.opsPerSecond, 0) / searchFilter.length + }; + } + + // Process index operations + if (indexOps && indexOps.length > 0) { + summary.categories.indexOperations = { + testCount: indexOps.length, + totalTime: indexOps.reduce((sum, test) => sum + test.totalTime, 0), + avgOpsPerSecond: indexOps.reduce((sum, test) => sum + test.opsPerSecond, 0) / indexOps.length + }; + } + + // Process memory results + if (memory && memory.results && memory.results.length > 0) { + summary.categories.memoryUsage = { + testCount: memory.results.length, + totalTime: memory.results.reduce((sum, test) => sum + test.executionTime, 0), + avgHeapDelta: memory.results.reduce((sum, test) => sum + test.memoryDelta.heapUsed, 0) / memory.results.length + }; + + // Find memory efficiency + memory.results.forEach(test => { + if (test.memoryDelta.heapUsed < summary.performance.mostMemoryEfficient.memoryUsed) { + summary.performance.mostMemoryEfficient = { + name: test.description, + memoryUsed: test.memoryDelta.heapUsed + }; + } + if (test.memoryDelta.heapUsed > summary.performance.leastMemoryEfficient.memoryUsed) { + summary.performance.leastMemoryEfficient = { + name: test.description, + memoryUsed: test.memoryDelta.heapUsed + }; + } + }); + } + + // Process comparison results + if (comparison && comparison.allResults && comparison.allResults.length > 0) { + summary.categories.comparison = { + testCount: comparison.allResults.length, + totalTime: comparison.allResults.reduce((sum, test) => sum + test.totalTime, 0), + avgOpsPerSecond: comparison.allResults.reduce((sum, test) => sum + test.opsPerSecond, 0) / comparison.allResults.length + }; + } + + // Process utility operations + if (utilities && utilities.length > 0) { + summary.categories.utilityOperations = { + testCount: utilities.length, + totalTime: utilities.reduce((sum, test) => sum + test.totalTime, 0), + avgOpsPerSecond: utilities.reduce((sum, test) => sum + test.opsPerSecond, 0) / utilities.length + }; + } + + // Process pagination results + if (pagination && pagination.length > 0) { + summary.categories.pagination = { + testCount: pagination.length, + totalTime: pagination.reduce((sum, test) => sum + test.totalTime, 0), + avgOpsPerSecond: pagination.reduce((sum, test) => sum + test.opsPerSecond, 0) / pagination.length + }; + } + + // Process persistence results + if (persistence && persistence.length > 0) { + summary.categories.persistence = { + testCount: persistence.length, + totalTime: persistence.reduce((sum, test) => sum + test.totalTime, 0), + avgOpsPerSecond: persistence.filter(test => test.opsPerSecond > 0).reduce((sum, test) => sum + test.opsPerSecond, 0) / persistence.filter(test => test.opsPerSecond > 0).length || 0 + }; + } + + // Process immutable comparison results + if (immutableComparison && immutableComparison.length > 0) { + summary.categories.immutableComparison = { + testCount: immutableComparison.length, + totalTime: immutableComparison.reduce((sum, test) => sum + test.totalTime, 0), + avgOpsPerSecond: immutableComparison.filter(test => test.opsPerSecond > 0).reduce((sum, test) => sum + test.opsPerSecond, 0) / immutableComparison.filter(test => test.opsPerSecond > 0).length || 0 + }; + } + + // Calculate totals + summary.totalTests = Object.values(summary.categories).reduce((sum, cat) => sum + cat.testCount, 0); + summary.totalTime = Object.values(summary.categories).reduce((sum, cat) => sum + cat.totalTime, 0); + + // Generate recommendations + if (summary.categories.basicOperations && summary.categories.basicOperations.avgOpsPerSecond > 10000) { + summary.recommendations.push("✅ Basic operations performance is excellent for most use cases"); + } + + if (summary.categories.indexOperations && summary.categories.searchFilter) { + const indexAvg = summary.categories.indexOperations.avgOpsPerSecond; + const searchAvg = summary.categories.searchFilter.avgOpsPerSecond; + if (indexAvg > searchAvg * 2) { + summary.recommendations.push("💡 Consider using indexed queries (find) instead of filters for better performance"); + } + } + + if (summary.categories.memoryUsage && summary.categories.memoryUsage.avgHeapDelta < 10) { + summary.recommendations.push("✅ Memory usage is efficient for typical workloads"); + } else if (summary.categories.memoryUsage && summary.categories.memoryUsage.avgHeapDelta > 50) { + summary.recommendations.push("⚠️ Consider optimizing memory usage for large datasets"); + } + + if (summary.categories.comparison) { + summary.recommendations.push("📊 Review comparison results to understand trade-offs vs native structures"); + } + + if (summary.categories.utilityOperations && summary.categories.utilityOperations.avgOpsPerSecond > 1000) { + summary.recommendations.push("✅ Utility operations (clone, merge, freeze) perform well"); + } + + if (summary.categories.pagination && summary.categories.pagination.avgOpsPerSecond > 100) { + summary.recommendations.push("✅ Pagination performance is suitable for typical UI requirements"); + } + + if (summary.categories.persistence) { + summary.recommendations.push("💾 Persistence operations available for data serialization needs"); + } + + if (summary.categories.immutableComparison) { + summary.recommendations.push("🔒 Review immutable vs mutable comparison for data safety vs performance trade-offs"); + } + + return summary; +} + +/** + * Prints the summary report + * @param {Object} summary - Summary report object + */ +function printSummaryReport (summary) { + console.log("\n" + "=".repeat(80)); + console.log("🎯 HARO BENCHMARK SUMMARY REPORT"); + console.log("=".repeat(80)); + + console.log("\n📊 OVERVIEW:"); + console.log(` Total Tests: ${summary.totalTests}`); + console.log(` Total Time: ${formatDuration(summary.totalTime)}`); + console.log(` Categories: ${Object.keys(summary.categories).length}`); + + console.log("\n🏆 PERFORMANCE HIGHLIGHTS:"); + console.log(` Fastest Operation: ${summary.performance.fastest.name}`); + console.log(` └── ${summary.performance.fastest.opsPerSecond.toLocaleString()} ops/second`); + console.log(` Slowest Operation: ${summary.performance.slowest.name}`); + console.log(` └── ${summary.performance.slowest.opsPerSecond.toLocaleString()} ops/second`); + + if (summary.performance.mostMemoryEfficient.memoryUsed !== Infinity) { + console.log("\n💾 MEMORY EFFICIENCY:"); + console.log(` Most Efficient: ${summary.performance.mostMemoryEfficient.name}`); + console.log(` └── ${summary.performance.mostMemoryEfficient.memoryUsed.toFixed(2)} MB`); + console.log(` Least Efficient: ${summary.performance.leastMemoryEfficient.name}`); + console.log(` └── ${summary.performance.leastMemoryEfficient.memoryUsed.toFixed(2)} MB`); + } + + console.log("\n📋 CATEGORY BREAKDOWN:"); + Object.entries(summary.categories).forEach(([category, stats]) => { + console.log(` ${category}:`); + console.log(` ├── Tests: ${stats.testCount}`); + console.log(` ├── Time: ${formatDuration(stats.totalTime)}`); + if (stats.avgOpsPerSecond) { + console.log(` └── Avg Performance: ${stats.avgOpsPerSecond.toFixed(0)} ops/second`); + } else if (stats.avgHeapDelta) { + console.log(` └── Avg Memory: ${stats.avgHeapDelta.toFixed(2)} MB`); + } + }); + + if (summary.recommendations.length > 0) { + console.log("\n💡 RECOMMENDATIONS:"); + summary.recommendations.forEach(rec => { + console.log(` ${rec}`); + }); + } + + console.log("\n" + "=".repeat(80)); + console.log("🏁 BENCHMARK COMPLETE"); + console.log("=".repeat(80) + "\n"); +} + +/** + * Main function to run all benchmarks + * @param {Object} options - Benchmark options + * @returns {Object} All benchmark results + */ +async function runAllBenchmarks (options = {}) { + const { + includeBasic = true, + includeSearch = true, + includeIndex = true, + includeMemory = true, + includeComparison = true, + includeUtilities = true, + includePagination = true, + includePersistence = true, + includeImmutableComparison = true, + verbose = true + } = options; + + const results = {}; + const startTime = Date.now(); + + console.log("🚀 Starting Haro Benchmark Suite...\n"); + console.log("📋 Benchmark Configuration:"); + console.log(` Node.js Version: ${process.version}`); + console.log(` Platform: ${process.platform}`); + console.log(` Architecture: ${process.arch}`); + console.log(` Memory: ${Math.round(process.memoryUsage().heapTotal / 1024 / 1024)} MB available\n`); + + try { + // Run basic operations benchmarks + if (includeBasic) { + if (verbose) console.log("⏳ Running basic operations benchmarks..."); + results.basicOps = runBasicOperationsBenchmarks(); + if (verbose) console.log("✅ Basic operations benchmarks completed\n"); + } + + // Run search and filter benchmarks + if (includeSearch) { + if (verbose) console.log("⏳ Running search and filter benchmarks..."); + results.searchFilter = runSearchFilterBenchmarks(); + if (verbose) console.log("✅ Search and filter benchmarks completed\n"); + } + + // Run index operations benchmarks + if (includeIndex) { + if (verbose) console.log("⏳ Running index operations benchmarks..."); + results.indexOps = runIndexOperationsBenchmarks(); + if (verbose) console.log("✅ Index operations benchmarks completed\n"); + } + + // Run memory benchmarks + if (includeMemory) { + if (verbose) console.log("⏳ Running memory usage benchmarks..."); + results.memory = runMemoryBenchmarks(); + if (verbose) console.log("✅ Memory usage benchmarks completed\n"); + } + + // Run comparison benchmarks + if (includeComparison) { + if (verbose) console.log("⏳ Running comparison benchmarks..."); + results.comparison = runComparisonBenchmarks(); + if (verbose) console.log("✅ Comparison benchmarks completed\n"); + } + + // Run utility operations benchmarks + if (includeUtilities) { + if (verbose) console.log("⏳ Running utility operations benchmarks..."); + results.utilities = runUtilityOperationsBenchmarks(); + if (verbose) console.log("✅ Utility operations benchmarks completed\n"); + } + + // Run pagination benchmarks + if (includePagination) { + if (verbose) console.log("⏳ Running pagination benchmarks..."); + results.pagination = runPaginationBenchmarks(); + if (verbose) console.log("✅ Pagination benchmarks completed\n"); + } + + // Run persistence benchmarks + if (includePersistence) { + if (verbose) console.log("⏳ Running persistence benchmarks..."); + results.persistence = runPersistenceBenchmarks(); + if (verbose) console.log("✅ Persistence benchmarks completed\n"); + } + + // Run immutable vs mutable comparison benchmarks + if (includeImmutableComparison) { + if (verbose) console.log("⏳ Running immutable vs mutable comparison benchmarks..."); + results.immutableComparison = runImmutableComparisonBenchmarks(); + if (verbose) console.log("✅ Immutable vs mutable comparison benchmarks completed\n"); + } + + const endTime = Date.now(); + const totalDuration = endTime - startTime; + + // Generate and print summary + const summary = generateSummaryReport(results); + summary.totalDuration = totalDuration; + + if (verbose) { + printSummaryReport(summary); + } + + return { results, summary }; + + } catch (error) { + console.error("❌ Benchmark suite failed:", error); + throw error; + } +} + +/** + * CLI argument parser + * @returns {Object} Parsed CLI options + */ +function parseCliArguments () { + const args = process.argv.slice(2); + const options = { + includeBasic: true, + includeSearch: true, + includeIndex: true, + includeMemory: true, + includeComparison: true, + includeUtilities: true, + includePagination: true, + includePersistence: true, + includeImmutableComparison: true, + verbose: true + }; + + // Helper function to disable all categories except the specified one + const runOnlyCategory = category => { + Object.keys(options).forEach(key => { + if (key.startsWith("include") && key !== category) { + options[key] = false; + } + }); + }; + + args.forEach(arg => { + switch (arg) { // eslint-disable-line default-case + case "--basic-only": + runOnlyCategory("includeBasic"); + break; + case "--search-only": + runOnlyCategory("includeSearch"); + break; + case "--index-only": + runOnlyCategory("includeIndex"); + break; + case "--memory-only": + runOnlyCategory("includeMemory"); + break; + case "--comparison-only": + runOnlyCategory("includeComparison"); + break; + case "--utilities-only": + runOnlyCategory("includeUtilities"); + break; + case "--pagination-only": + runOnlyCategory("includePagination"); + break; + case "--persistence-only": + runOnlyCategory("includePersistence"); + break; + case "--immutable-only": + runOnlyCategory("includeImmutableComparison"); + break; + case "--core-only": + // Run only core benchmarks (basic, search, index) + options.includeMemory = false; + options.includeComparison = false; + options.includeUtilities = false; + options.includePagination = false; + options.includePersistence = false; + options.includeImmutableComparison = false; + break; + case "--advanced-only": + // Run only advanced benchmarks + options.includeBasic = false; + options.includeSearch = false; + options.includeIndex = false; + break; + case "--no-basic": + options.includeBasic = false; + break; + case "--no-search": + options.includeSearch = false; + break; + case "--no-index": + options.includeIndex = false; + break; + case "--no-memory": + options.includeMemory = false; + break; + case "--no-comparison": + options.includeComparison = false; + break; + case "--no-utilities": + options.includeUtilities = false; + break; + case "--no-pagination": + options.includePagination = false; + break; + case "--no-persistence": + options.includePersistence = false; + break; + case "--no-immutable": + options.includeImmutableComparison = false; + break; + case "--quiet": + options.verbose = false; + break; + case "--help": + console.log(` +Haro Benchmark Suite v16.0.0 + +Usage: node benchmarks/index.js [options] + +SINGLE CATEGORY OPTIONS: + --basic-only Run only basic CRUD operations benchmarks + --search-only Run only search and filter benchmarks + --index-only Run only index operations benchmarks + --memory-only Run only memory usage benchmarks + --comparison-only Run only vs native structures benchmarks + --utilities-only Run only utility operations benchmarks (clone, merge, freeze, etc.) + --pagination-only Run only pagination/limit benchmarks + --persistence-only Run only dump/override persistence benchmarks + --immutable-only Run only immutable vs mutable comparison benchmarks + +CATEGORY GROUP OPTIONS: + --core-only Run only core benchmarks (basic, search, index) + --advanced-only Run only advanced benchmarks (memory, comparison, utilities, etc.) + +EXCLUSION OPTIONS: + --no-basic Exclude basic operations benchmarks + --no-search Exclude search and filter benchmarks + --no-index Exclude index operations benchmarks + --no-memory Exclude memory usage benchmarks + --no-comparison Exclude comparison benchmarks + --no-utilities Exclude utility operations benchmarks + --no-pagination Exclude pagination benchmarks + --no-persistence Exclude persistence benchmarks + --no-immutable Exclude immutable vs mutable benchmarks + +OUTPUT OPTIONS: + --quiet Suppress verbose output + --help Show this help message + +BENCHMARK CATEGORIES: + Basic Operations CRUD operations (set, get, delete, batch) + Search & Filter Query operations (find, filter, search, where) + Index Operations Indexing performance and benefits + Memory Usage Memory consumption and efficiency analysis + Comparison Performance vs native JavaScript structures + Utility Operations Helper methods (clone, merge, freeze, forEach, uuid) + Pagination Limit-based pagination performance + Persistence Dump/override operations for data serialization + Immutable Comparison Performance comparison between mutable and immutable modes + +Examples: + node benchmarks/index.js # Run all benchmarks + node benchmarks/index.js --basic-only # Run basic operations only + node benchmarks/index.js --core-only # Run core benchmarks only + node benchmarks/index.js --no-memory # Run all except memory benchmarks + node benchmarks/index.js --quiet # Run all benchmarks quietly + node benchmarks/index.js --utilities-only # Test utility methods only + `); + process.exit(0); + break; + } + }); + + return options; +} + +// Run benchmarks if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + const options = parseCliArguments(); + runAllBenchmarks(options).catch(error => { + console.error("Fatal error:", error); + process.exit(1); + }); +} + +export { runAllBenchmarks, generateSummaryReport }; diff --git a/benchmarks/memory-usage.js b/benchmarks/memory-usage.js new file mode 100644 index 00000000..edbd37a0 --- /dev/null +++ b/benchmarks/memory-usage.js @@ -0,0 +1,543 @@ +import { performance } from "node:perf_hooks"; +import { haro } from "../dist/haro.js"; +import { generateIndexTestData } from "./index-operations.js"; + +/** + * Gets current memory usage information + * @returns {Object} Memory usage information + */ +function getMemoryUsage () { + const memUsage = process.memoryUsage(); + + return { + rss: memUsage.rss / 1024 / 1024, // MB + heapUsed: memUsage.heapUsed / 1024 / 1024, // MB + heapTotal: memUsage.heapTotal / 1024 / 1024, // MB + external: memUsage.external / 1024 / 1024, // MB + arrayBuffers: memUsage.arrayBuffers / 1024 / 1024 // MB + }; +} + +/** + * Forces garbage collection if possible + */ +function forceGC () { + if (global.gc) { + global.gc(); + } +} + +/** + * Measures memory usage of a function + * @param {Function} fn - Function to measure + * @param {string} description - Description of the test + * @returns {Object} Memory usage results + */ +function measureMemory (fn, description) { + forceGC(); + const startMemory = getMemoryUsage(); + + const startTime = performance.now(); + const result = fn(); + const endTime = performance.now(); + + forceGC(); + const endMemory = getMemoryUsage(); + + return { + description, + executionTime: endTime - startTime, + memoryBefore: startMemory, + memoryAfter: endMemory, + memoryDelta: { + rss: endMemory.rss - startMemory.rss, + heapUsed: endMemory.heapUsed - startMemory.heapUsed, + heapTotal: endMemory.heapTotal - startMemory.heapTotal, + external: endMemory.external - startMemory.external, + arrayBuffers: endMemory.arrayBuffers - startMemory.arrayBuffers + }, + result + }; +} + +/** + * Benchmarks memory usage during store creation + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of memory benchmark results + */ +function benchmarkCreationMemory (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Test basic store creation + const basicCreationResult = measureMemory(() => { + return haro(testData); + }, `Basic store creation (${size} records)`); + results.push(basicCreationResult); + + // Test store creation with indexes + const indexedCreationResult = measureMemory(() => { + return haro(testData, { + index: ["category", "status", "priority", "region", "userId"] + }); + }, `Indexed store creation (${size} records)`); + results.push(indexedCreationResult); + + // Test store creation with complex indexes + const complexIndexCreationResult = measureMemory(() => { + return haro(testData, { + index: [ + "category", "status", "priority", "region", "userId", + "category|status", "region|priority", "userId|category", + "category|status|priority" + ] + }); + }, `Complex indexed store creation (${size} records)`); + results.push(complexIndexCreationResult); + + // Test store creation with versioning + const versioningCreationResult = measureMemory(() => { + return haro(testData, { + versioning: true, + index: ["category", "status"] + }); + }, `Versioning store creation (${size} records)`); + results.push(versioningCreationResult); + }); + + return results; +} + +/** + * Benchmarks memory usage during data operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of memory benchmark results + */ +function benchmarkOperationMemory (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Test SET operations memory usage + const setOperationResult = measureMemory(() => { + const store = haro(); + for (let i = 0; i < Math.min(size, 1000); i++) { + store.set(i, testData[i]); + } + + return store; + }, `SET operations memory (${Math.min(size, 1000)} records)`); + results.push(setOperationResult); + + // Test BATCH operations memory usage + const batchOperationResult = measureMemory(() => { + const store = haro(); + store.batch(testData, "set"); + + return store; + }, `BATCH operations memory (${size} records)`); + results.push(batchOperationResult); + + // Test DELETE operations memory usage + const deleteOperationResult = measureMemory(() => { + const store = haro(testData); + const keys = Array.from(store.keys()); + for (let i = 0; i < Math.min(keys.length, 100); i++) { + try { + store.del(keys[i]); + } catch (e) { // eslint-disable-line no-unused-vars + // Record might already be deleted + } + } + + return store; + }, `DELETE operations memory (${Math.min(size, 100)} deletions)`); + results.push(deleteOperationResult); + + // Test CLEAR operations memory usage + const clearOperationResult = measureMemory(() => { + const store = haro(testData); + store.clear(); + + return store; + }, `CLEAR operations memory (${size} records)`); + results.push(clearOperationResult); + }); + + return results; +} + +/** + * Benchmarks memory usage during query operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of memory benchmark results + */ +function benchmarkQueryMemory (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + const store = haro(testData, { + index: ["category", "status", "priority", "category|status"] + }); + + // Test FIND operations memory usage + const findOperationResult = measureMemory(() => { + const results = []; // eslint-disable-line no-shadow + for (let i = 0; i < 100; i++) { + results.push(store.find({ category: "A" })); + } + + return results; + }, `FIND operations memory (${size} records, 100 queries)`); + results.push(findOperationResult); + + // Test FILTER operations memory usage + const filterOperationResult = measureMemory(() => { + const results = []; // eslint-disable-line no-shadow + for (let i = 0; i < 10; i++) { + results.push(store.filter(record => record.category === "A" && record.status === "active")); + } + + return results; + }, `FILTER operations memory (${size} records, 10 queries)`); + results.push(filterOperationResult); + + // Test SEARCH operations memory usage + const searchOperationResult = measureMemory(() => { + const results = []; // eslint-disable-line no-shadow + for (let i = 0; i < 50; i++) { + results.push(store.search("A", "category")); + } + + return results; + }, `SEARCH operations memory (${size} records, 50 queries)`); + results.push(searchOperationResult); + + // Test MAP operations memory usage + const mapOperationResult = measureMemory(() => { + return store.map(record => ({ + id: record.id, + category: record.category, + status: record.status + })); + }, `MAP operations memory (${size} records)`); + results.push(mapOperationResult); + }); + + return results; +} + +/** + * Benchmarks memory usage during index operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of memory benchmark results + */ +function benchmarkIndexMemory (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Test index creation memory usage + const indexCreationResult = measureMemory(() => { + const store = haro(testData); + store.reindex("category"); + store.reindex("status"); + store.reindex("priority"); + + return store; + }, `Index creation memory (${size} records)`); + results.push(indexCreationResult); + + // Test composite index creation memory usage + const compositeIndexResult = measureMemory(() => { + const store = haro(testData); + store.reindex("category|status"); + store.reindex("region|priority"); + + return store; + }, `Composite index memory (${size} records)`); + results.push(compositeIndexResult); + + // Test index dump memory usage + const indexDumpResult = measureMemory(() => { + const store = haro(testData, { + index: ["category", "status", "priority", "category|status"] + }); + + return store.dump("indexes"); + }, `Index dump memory (${size} records)`); + results.push(indexDumpResult); + + // Test index override memory usage + const indexOverrideResult = measureMemory(() => { + const store = haro(testData, { + index: ["category", "status", "priority"] + }); + const indexData = store.dump("indexes"); + const newStore = haro(); + newStore.override(indexData, "indexes"); + + return newStore; + }, `Index override memory (${size} records)`); + results.push(indexOverrideResult); + }); + + return results; +} + +/** + * Benchmarks memory usage with versioning enabled + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of memory benchmark results + */ +function benchmarkVersioningMemory (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Test versioning store creation + const versioningCreationResult = measureMemory(() => { + return haro(testData, { versioning: true }); + }, `Versioning store creation (${size} records)`); + results.push(versioningCreationResult); + + // Test versioning with updates + const versioningUpdatesResult = measureMemory(() => { + const store = haro(testData, { versioning: true }); + + // Update records multiple times to create versions + for (let i = 0; i < Math.min(size, 100); i++) { + for (let version = 0; version < 5; version++) { + store.set(i, { + ...testData[i], + version: version, + updated: new Date() + }); + } + } + + return store; + }, `Versioning with updates (${Math.min(size, 100)} records, 5 versions each)`); + results.push(versioningUpdatesResult); + }); + + return results; +} + +/** + * Benchmarks memory usage under stress conditions + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of memory benchmark results + */ +function benchmarkStressMemory (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateIndexTestData(size); + + // Test rapid creation and destruction + const rapidCycleResult = measureMemory(() => { + const stores = []; + for (let i = 0; i < 10; i++) { + stores.push(haro(testData.slice(0, size / 10))); + } + + // Clear all stores + stores.forEach(store => store.clear()); + + return stores; + }, `Rapid store cycles (${size} records)`); + results.push(rapidCycleResult); + + // Test memory with large result sets + const largeResultSetResult = measureMemory(() => { + const store = haro(testData, { index: ["category"] }); + const results = []; // eslint-disable-line no-shadow + + // Create multiple large result sets + for (let i = 0; i < 5; i++) { + results.push(store.toArray()); + results.push(store.dump("records")); + results.push(store.dump("indexes")); + } + + return results; + }, `Large result sets (${size} records)`); + results.push(largeResultSetResult); + }); + + return results; +} + +/** + * Analyzes memory growth over time + * @param {number} dataSize - Size of test data + * @returns {Object} Memory growth analysis + */ +function analyzeMemoryGrowth (dataSize) { + const testData = generateIndexTestData(dataSize); + const memorySnapshots = []; + + // Initial memory + forceGC(); + memorySnapshots.push({ + operation: "Initial", + memory: getMemoryUsage() + }); + + // Create store + const store = haro(); + forceGC(); + memorySnapshots.push({ + operation: "Store created", + memory: getMemoryUsage() + }); + + // Add data in batches + const batchSize = Math.floor(dataSize / 10); + for (let i = 0; i < 10; i++) { + const batch = testData.slice(i * batchSize, (i + 1) * batchSize); + store.batch(batch, "set"); + + forceGC(); + memorySnapshots.push({ + operation: `Batch ${i + 1} added`, + memory: getMemoryUsage() + }); + } + + // Add indexes + store.reindex("category"); + store.reindex("status"); + store.reindex("priority"); + + forceGC(); + memorySnapshots.push({ + operation: "Indexes added", + memory: getMemoryUsage() + }); + + // Perform queries + for (let i = 0; i < 100; i++) { + store.find({ category: "A" }); + store.filter(record => record.status === "active"); + } + + forceGC(); + memorySnapshots.push({ + operation: "After queries", + memory: getMemoryUsage() + }); + + // Clear store + store.clear(); + + forceGC(); + memorySnapshots.push({ + operation: "After clear", + memory: getMemoryUsage() + }); + + return { + dataSize, + snapshots: memorySnapshots, + maxHeapUsed: Math.max(...memorySnapshots.map(s => s.memory.heapUsed)), + totalGrowth: memorySnapshots[memorySnapshots.length - 1].memory.heapUsed - memorySnapshots[0].memory.heapUsed + }; +} + +/** + * Prints memory benchmark results + * @param {Array} results - Array of memory benchmark results + */ +function printMemoryResults (results) { + console.log("\n=== MEMORY USAGE BENCHMARK RESULTS ===\n"); + + console.log("Operation".padEnd(50) + "Execution Time".padEnd(16) + "Heap Delta (MB)".padEnd(16) + "RSS Delta (MB)"); + console.log("-".repeat(98)); + + results.forEach(result => { + const name = result.description.padEnd(50); + const execTime = result.executionTime.toFixed(2).padEnd(16); + const heapDelta = result.memoryDelta.heapUsed.toFixed(2).padEnd(16); + const rssDelta = result.memoryDelta.rss.toFixed(2); + + console.log(name + execTime + heapDelta + rssDelta); + }); + + console.log("\n"); +} + +/** + * Prints memory growth analysis + * @param {Object} analysis - Memory growth analysis results + */ +function printMemoryGrowthAnalysis (analysis) { + console.log("\n=== MEMORY GROWTH ANALYSIS ===\n"); + console.log(`Data Size: ${analysis.dataSize} records`); + console.log(`Max Heap Used: ${analysis.maxHeapUsed.toFixed(2)} MB`); + console.log(`Total Growth: ${analysis.totalGrowth.toFixed(2)} MB`); + console.log("\nMemory Snapshots:"); + + analysis.snapshots.forEach((snapshot, index) => { + const operation = snapshot.operation.padEnd(20); + const heapUsed = snapshot.memory.heapUsed.toFixed(2).padEnd(10); + const rss = snapshot.memory.rss.toFixed(2).padEnd(10); + const delta = index > 0 ? + (snapshot.memory.heapUsed - analysis.snapshots[index - 1].memory.heapUsed).toFixed(2) : + "0.00"; + + console.log(`${operation} | Heap: ${heapUsed} MB | RSS: ${rss} MB | Delta: ${delta} MB`); + }); + + console.log("\n"); +} + +/** + * Main function to run all memory benchmarks + */ +function runMemoryBenchmarks () { + console.log("💾 Running Memory Usage Benchmarks...\n"); + + const dataSizes = [1000, 10000, 25000]; + const allResults = []; + + console.log("Testing creation memory usage..."); + allResults.push(...benchmarkCreationMemory(dataSizes)); + + console.log("Testing operation memory usage..."); + allResults.push(...benchmarkOperationMemory(dataSizes)); + + console.log("Testing query memory usage..."); + allResults.push(...benchmarkQueryMemory(dataSizes)); + + console.log("Testing index memory usage..."); + allResults.push(...benchmarkIndexMemory(dataSizes)); + + console.log("Testing versioning memory usage..."); + allResults.push(...benchmarkVersioningMemory(dataSizes)); + + console.log("Testing stress memory usage..."); + allResults.push(...benchmarkStressMemory(dataSizes)); + + printMemoryResults(allResults); + + console.log("Analyzing memory growth..."); + const growthAnalysis = analyzeMemoryGrowth(10000); + printMemoryGrowthAnalysis(growthAnalysis); + + return { results: allResults, growthAnalysis }; +} + +// Run benchmarks if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runMemoryBenchmarks(); +} + +export { runMemoryBenchmarks, getMemoryUsage, analyzeMemoryGrowth }; diff --git a/benchmarks/pagination.js b/benchmarks/pagination.js new file mode 100644 index 00000000..8d0332f4 --- /dev/null +++ b/benchmarks/pagination.js @@ -0,0 +1,428 @@ +import { performance } from "node:perf_hooks"; +import { haro } from "../dist/haro.js"; + +/** + * Generates test data for pagination benchmarking + * @param {number} size - Number of records to generate + * @returns {Array} Array of test records optimized for pagination testing + */ +function generatePaginationTestData (size) { + const data = []; + const categories = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]; + const statuses = ["active", "inactive", "pending", "archived"]; + + for (let i = 0; i < size; i++) { + data.push({ + id: i, + name: `Item ${i}`, + category: categories[i % categories.length], + status: statuses[i % statuses.length], + priority: Math.floor(Math.random() * 5) + 1, + score: Math.floor(Math.random() * 1000), + timestamp: new Date(2024, 0, 1, 0, 0, 0, i * 1000), + description: `Description for item ${i}`, + tags: [`tag${i % 20}`, `group${i % 10}`], + metadata: { + level: Math.floor(i / 100), + region: `Region ${i % 5}`, + department: `Dept ${i % 15}` + } + }); + } + + return data; +} + +/** + * Runs a benchmark test and returns timing information + * @param {string} name - Name of the test + * @param {Function} fn - Function to benchmark + * @param {number} iterations - Number of iterations to run + * @returns {Object} Benchmark results + */ +function benchmark (name, fn, iterations = 100) { + const start = performance.now(); + for (let i = 0; i < iterations; i++) { + fn(); + } + const end = performance.now(); + const total = end - start; + const avgTime = total / iterations; + + return { + name, + iterations, + totalTime: total, + avgTime, + opsPerSecond: Math.floor(1000 / avgTime) + }; +} + +/** + * Benchmarks basic limit operations with different page sizes + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkBasicLimitOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePaginationTestData(size); + const store = haro(testData); + + // Small page sizes + const smallPageResult = benchmark(`LIMIT small page (10 items from ${size} records)`, () => { + store.limit(0, 10); + }); + results.push(smallPageResult); + + // Medium page sizes + const mediumPageResult = benchmark(`LIMIT medium page (50 items from ${size} records)`, () => { + store.limit(0, 50); + }); + results.push(mediumPageResult); + + // Large page sizes + const largePageResult = benchmark(`LIMIT large page (100 items from ${size} records)`, () => { + store.limit(0, 100); + }); + results.push(largePageResult); + + // Very large page sizes + const veryLargePageResult = benchmark(`LIMIT very large page (1000 items from ${size} records)`, () => { + store.limit(0, Math.min(1000, size)); + }); + results.push(veryLargePageResult); + }); + + return results; +} + +/** + * Benchmarks offset-based pagination patterns + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkOffsetPagination (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePaginationTestData(size); + const store = haro(testData); + const pageSize = 20; + + // First page (offset 0) + const firstPageResult = benchmark(`LIMIT first page (offset 0, ${pageSize} items)`, () => { + store.limit(0, pageSize); + }); + results.push(firstPageResult); + + // Middle page + const middleOffset = Math.floor(size / 2); + const middlePageResult = benchmark(`LIMIT middle page (offset ${middleOffset}, ${pageSize} items)`, () => { + store.limit(middleOffset, pageSize); + }); + results.push(middlePageResult); + + // Near end page + const nearEndOffset = Math.max(0, size - pageSize * 2); + const nearEndPageResult = benchmark(`LIMIT near end page (offset ${nearEndOffset}, ${pageSize} items)`, () => { + store.limit(nearEndOffset, pageSize); + }); + results.push(nearEndPageResult); + + // Last page (potentially partial) + const lastOffset = Math.max(0, size - pageSize); + const lastPageResult = benchmark(`LIMIT last page (offset ${lastOffset}, ${pageSize} items)`, () => { + store.limit(lastOffset, pageSize); + }); + results.push(lastPageResult); + + // Beyond data bounds (should return empty) + const beyondBoundsResult = benchmark(`LIMIT beyond bounds (offset ${size + 100}, ${pageSize} items)`, () => { + store.limit(size + 100, pageSize); + }); + results.push(beyondBoundsResult); + }); + + return results; +} + +/** + * Benchmarks pagination with different page sizes to find optimal sizes + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkPageSizeOptimization (dataSizes) { + const results = []; + const pageSizes = [1, 5, 10, 20, 50, 100, 200, 500, 1000]; + + dataSizes.forEach(size => { + const testData = generatePaginationTestData(size); + const store = haro(testData); + + pageSizes.forEach(pageSize => { + if (pageSize <= size) { + const pageSizeResult = benchmark(`LIMIT page size ${pageSize} (${size} total records)`, () => { + store.limit(0, pageSize); + }); + results.push(pageSizeResult); + } + }); + }); + + return results; +} + +/** + * Benchmarks pagination with raw vs processed data + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkPaginationModes (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePaginationTestData(size); + + // Test with immutable store + const immutableStore = haro(testData, { immutable: true }); + const immutableResult = benchmark(`LIMIT immutable mode (50 items from ${size} records)`, () => { + immutableStore.limit(0, 50); + }); + results.push(immutableResult); + + // Test with mutable store + const mutableStore = haro(testData, { immutable: false }); + const mutableResult = benchmark(`LIMIT mutable mode (50 items from ${size} records)`, () => { + mutableStore.limit(0, 50); + }); + results.push(mutableResult); + + // Test with raw data + const rawResult = benchmark(`LIMIT raw data (50 items from ${size} records)`, () => { + mutableStore.limit(0, 50, true); + }); + results.push(rawResult); + + // Test with processed data + const processedResult = benchmark(`LIMIT processed data (50 items from ${size} records)`, () => { + mutableStore.limit(0, 50, false); + }); + results.push(processedResult); + }); + + return results; +} + +/** + * Benchmarks sequential pagination patterns (like browsing through pages) + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkSequentialPagination (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePaginationTestData(size); + const store = haro(testData); + const pageSize = 25; + const totalPages = Math.ceil(size / pageSize); + + // Simulate browsing through first 10 pages + const pagesToTest = Math.min(10, totalPages); + const sequentialResult = benchmark(`LIMIT sequential pagination (${pagesToTest} pages, ${pageSize} items each)`, () => { + for (let page = 0; page < pagesToTest; page++) { + const offset = page * pageSize; + store.limit(offset, pageSize); + } + }, 1); + results.push(sequentialResult); + + // Simulate random page access pattern + const randomPagesResult = benchmark(`LIMIT random page access (10 random pages, ${pageSize} items each)`, () => { + for (let i = 0; i < 10; i++) { + const randomPage = Math.floor(Math.random() * totalPages); + const offset = randomPage * pageSize; + store.limit(offset, pageSize); + } + }, 1); + results.push(randomPagesResult); + }); + + return results; +} + +/** + * Benchmarks pagination combined with other operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkPaginationWithOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePaginationTestData(size); + const store = haro(testData, { + index: ["category", "status", "priority"] + }); + + // Pagination after filtering + const paginateAfterFilterResult = benchmark(`LIMIT after filter (${size} records)`, () => { + const filtered = store.filter(record => record.priority > 3); + // Simulate pagination on filtered results by taking first 20 + + return filtered.slice(0, 20); + }); + results.push(paginateAfterFilterResult); + + // Pagination after find operation + const paginateAfterFindResult = benchmark(`LIMIT after find (${size} records)`, () => { + const found = store.find({ category: "A" }); + // Simulate pagination on found results by taking first 20 + + return found.slice(0, 20); + }); + results.push(paginateAfterFindResult); + + // Combined operations: find + sort + paginate simulation + const combinedOperationsResult = benchmark(`Combined find + sort + limit (${size} records)`, () => { + const found = store.find({ status: "active" }); + const sorted = found.sort((a, b) => b.score - a.score); + + return sorted.slice(0, 20); // Simulate limit + }); + results.push(combinedOperationsResult); + }); + + return results; +} + +/** + * Tests pagination memory efficiency + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkPaginationMemory (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePaginationTestData(size); + const store = haro(testData); + + // Memory usage pattern: get full dataset vs paginated chunks + if (global.gc) { + global.gc(); + } + const memBefore = process.memoryUsage().heapUsed; + + // Test getting all data at once + const allDataStart = performance.now(); + const allData = store.toArray(); // eslint-disable-line no-unused-vars + const allDataEnd = performance.now(); + + if (global.gc) { + global.gc(); + } + const memAfterAll = process.memoryUsage().heapUsed; + + // Test getting data in small chunks + const chunkSize = 100; + const chunksStart = performance.now(); + const chunks = []; + for (let offset = 0; offset < size; offset += chunkSize) { + chunks.push(store.limit(offset, chunkSize)); + } + const chunksEnd = performance.now(); + + if (global.gc) { + global.gc(); + } + const memAfterChunks = process.memoryUsage().heapUsed; + + results.push({ + name: `Memory comparison: all vs chunked (${size} records)`, + totalTime: allDataEnd - allDataStart + (chunksEnd - chunksStart), + allDataTime: allDataEnd - allDataStart, + chunkedTime: chunksEnd - chunksStart, + memoryAllData: (memAfterAll - memBefore) / 1024 / 1024, // MB + memoryChunked: (memAfterChunks - memAfterAll) / 1024 / 1024, // MB + iterations: 1, + opsPerSecond: Math.floor(1000 / (allDataEnd - allDataStart + (chunksEnd - chunksStart))) + }); + }); + + return results; +} + +/** + * Prints formatted benchmark results + * @param {Array} results - Array of benchmark results + */ +function printResults (results) { + console.log("\n" + "=".repeat(80)); + console.log("PAGINATION BENCHMARK RESULTS"); + console.log("=".repeat(80)); + + results.forEach(result => { + const opsIndicator = result.opsPerSecond > 1000 ? "✅" : + result.opsPerSecond > 100 ? "🟡" : + result.opsPerSecond > 10 ? "🟠" : "🔴"; + + console.log(`${opsIndicator} ${result.name}`); + console.log(` ${result.opsPerSecond.toLocaleString()} ops/sec | ${result.totalTime.toFixed(2)}ms total | ${result.avgTime?.toFixed(4) || "N/A"}ms avg`); + + // Special formatting for memory results + if (result.memoryAllData !== undefined) { + console.log(` All data: ${result.allDataTime.toFixed(2)}ms, ${result.memoryAllData.toFixed(2)}MB`); + console.log(` Chunked: ${result.chunkedTime.toFixed(2)}ms, ${result.memoryChunked.toFixed(2)}MB`); + } + console.log(""); + }); +} + +/** + * Runs all pagination benchmarks + * @returns {Array} Array of all benchmark results + */ +function runPaginationBenchmarks () { + console.log("Starting Pagination Benchmarks...\n"); + + const dataSizes = [1000, 10000, 50000]; + let allResults = []; + + console.log("Testing basic limit operations..."); + allResults.push(...benchmarkBasicLimitOperations(dataSizes)); + + console.log("Testing offset pagination..."); + allResults.push(...benchmarkOffsetPagination(dataSizes)); + + console.log("Testing page size optimization..."); + allResults.push(...benchmarkPageSizeOptimization([10000])); // Test with medium size only + + console.log("Testing pagination modes..."); + allResults.push(...benchmarkPaginationModes(dataSizes)); + + console.log("Testing sequential pagination..."); + allResults.push(...benchmarkSequentialPagination(dataSizes)); + + console.log("Testing pagination with operations..."); + allResults.push(...benchmarkPaginationWithOperations(dataSizes)); + + console.log("Testing pagination memory efficiency..."); + allResults.push(...benchmarkPaginationMemory([1000, 10000])); // Smaller sizes for memory tests + + printResults(allResults); + + console.log("Pagination Benchmarks completed.\n"); + + return allResults; +} + +// Export for use in main benchmark runner +export { runPaginationBenchmarks }; + +// Run standalone if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runPaginationBenchmarks(); +} diff --git a/benchmarks/persistence.js b/benchmarks/persistence.js new file mode 100644 index 00000000..fa73cc12 --- /dev/null +++ b/benchmarks/persistence.js @@ -0,0 +1,489 @@ +import { performance } from "node:perf_hooks"; +import { haro } from "../dist/haro.js"; + +/** + * Generates test data for persistence benchmarking + * @param {number} size - Number of records to generate + * @returns {Array} Array of test records optimized for persistence testing + */ +function generatePersistenceTestData (size) { + const data = []; + const departments = ["Engineering", "Marketing", "Sales", "HR", "Finance", "Operations"]; + const locations = ["NYC", "SF", "LA", "Chicago", "Boston", "Austin"]; + + for (let i = 0; i < size; i++) { + data.push({ + id: i, + name: `Employee ${i}`, + email: `employee${i}@company.com`, + department: departments[i % departments.length], + location: locations[i % locations.length], + startDate: new Date(2020 + i % 4, i % 12, i % 28 + 1), + salary: 50000 + i % 100000, + active: Math.random() > 0.1, + skills: Array.from({ length: Math.floor(Math.random() * 5) + 1 }, + (_, j) => `skill${(i + j) % 20}`), + projects: Array.from({ length: Math.floor(i % 10) + 1 }, + (_, j) => ({ id: `proj${i}-${j}`, name: `Project ${i}-${j}` })), + metadata: { + created: new Date(), + updated: new Date(), + version: Math.floor(i / 1000) + 1, + tags: [`tag${i % 15}`, `category${i % 8}`], + preferences: { + theme: i % 2 === 0 ? "dark" : "light", + language: i % 3 === 0 ? "en" : i % 3 === 1 ? "es" : "fr", + timezone: `UTC${i % 24 - 12}` + } + } + }); + } + + return data; +} + +/** + * Runs a benchmark test and returns timing information + * @param {string} name - Name of the test + * @param {Function} fn - Function to benchmark + * @param {number} iterations - Number of iterations to run + * @returns {Object} Benchmark results + */ +function benchmark (name, fn, iterations = 10) { + const start = performance.now(); + for (let i = 0; i < iterations; i++) { + fn(); + } + const end = performance.now(); + const total = end - start; + const avgTime = total / iterations; + + return { + name, + iterations, + totalTime: total, + avgTime, + opsPerSecond: Math.floor(1000 / avgTime) + }; +} + +/** + * Benchmarks dump operations for different data types + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkDumpOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePersistenceTestData(size); + const store = haro(testData, { + index: ["department", "location", "active", "skills", "department|location", "active|department"] + }); + + // Dump records + const dumpRecordsResult = benchmark(`DUMP records (${size} records)`, () => { + return store.dump("records"); + }); + results.push(dumpRecordsResult); + + // Dump indexes + const dumpIndexesResult = benchmark(`DUMP indexes (${size} records)`, () => { + return store.dump("indexes"); + }); + results.push(dumpIndexesResult); + + // Test dump data size and structure + const recordsDump = store.dump("records"); + const indexesDump = store.dump("indexes"); + + results.push({ + name: `DUMP data analysis (${size} records)`, + iterations: 1, + totalTime: 0, + avgTime: 0, + opsPerSecond: 0, + recordsSize: recordsDump.length, + indexesSize: indexesDump.length, + recordsDataSize: JSON.stringify(recordsDump).length, + indexesDataSize: JSON.stringify(indexesDump).length + }); + }); + + return results; +} + +/** + * Benchmarks override operations with different data formats + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkOverrideOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePersistenceTestData(size); + const sourceStore = haro(testData, { + index: ["department", "location", "active", "skills"] + }); + + // Get dump data for override testing + const recordsDump = sourceStore.dump("records"); + const indexesDump = sourceStore.dump("indexes"); + + // Override with records + const overrideRecordsResult = benchmark(`OVERRIDE records (${size} records)`, () => { + const targetStore = haro(); + targetStore.override(recordsDump, "records"); + + return targetStore; + }); + results.push(overrideRecordsResult); + + // Override with indexes + const overrideIndexesResult = benchmark(`OVERRIDE indexes (${size} records)`, () => { + const targetStore = haro(testData); + targetStore.override(indexesDump, "indexes"); + + return targetStore; + }); + results.push(overrideIndexesResult); + + // Complete restoration (records + indexes) + const completeRestoreResult = benchmark(`OVERRIDE complete restore (${size} records)`, () => { + const targetStore = haro(); + targetStore.override(recordsDump, "records"); + targetStore.override(indexesDump, "indexes"); + + return targetStore; + }); + results.push(completeRestoreResult); + + // Validate restored data integrity + const targetStore = haro(); + targetStore.override(recordsDump, "records"); + targetStore.override(indexesDump, "indexes"); + + const integrityResult = { + name: `OVERRIDE integrity check (${size} records)`, + iterations: 1, + totalTime: 0, + avgTime: 0, + opsPerSecond: 0, + originalSize: sourceStore.size, + restoredSize: targetStore.size, + integrityMatch: sourceStore.size === targetStore.size, + sampleRecordMatch: JSON.stringify(sourceStore.get(0)) === JSON.stringify(targetStore.get(0)) + }; + results.push(integrityResult); + }); + + return results; +} + +/** + * Benchmarks round-trip persistence (dump + override) operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkRoundTripPersistence (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePersistenceTestData(size); + const sourceStore = haro(testData, { + index: ["department", "location", "active", "skills", "department|location"], + versioning: true + }); + + // Perform some operations to create versions + for (let i = 0; i < Math.min(10, size); i++) { + sourceStore.set(i.toString(), { ...testData[i], updated: new Date() }); + } + + // Round-trip with records only + const roundTripRecordsResult = benchmark(`Round-trip records only (${size} records)`, () => { + // Dump + const dump = sourceStore.dump("records"); + // Restore + const targetStore = haro(); + targetStore.override(dump, "records"); + + return targetStore; + }); + results.push(roundTripRecordsResult); + + // Round-trip with complete state (records + indexes) + const roundTripCompleteResult = benchmark(`Round-trip complete state (${size} records)`, () => { + // Dump both + const recordsDump = sourceStore.dump("records"); + const indexesDump = sourceStore.dump("indexes"); + // Restore + const targetStore = haro(); + targetStore.override(recordsDump, "records"); + targetStore.override(indexesDump, "indexes"); + + return targetStore; + }); + results.push(roundTripCompleteResult); + + // Test with different store configurations + const roundTripConfigResult = benchmark(`Round-trip with config restore (${size} records)`, () => { + const recordsDump = sourceStore.dump("records"); + const targetStore = haro(null, { + index: ["department", "location", "active"], + versioning: true, + immutable: true + }); + targetStore.override(recordsDump, "records"); + targetStore.reindex(); // Rebuild indexes with new config + + return targetStore; + }); + results.push(roundTripConfigResult); + }); + + return results; +} + +/** + * Benchmarks persistence with memory efficiency + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkPersistenceMemory (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generatePersistenceTestData(size); + + if (global.gc) { + global.gc(); + } + const memBefore = process.memoryUsage().heapUsed; + + // Create store and measure memory + const store = haro(testData, { + index: ["department", "location", "active", "skills"] + }); + + if (global.gc) { + global.gc(); + } + const memAfterCreate = process.memoryUsage().heapUsed; + + // Dump and measure memory impact + const dumpStart = performance.now(); + const recordsDump = store.dump("records"); + const indexesDump = store.dump("indexes"); + const dumpEnd = performance.now(); + + if (global.gc) { + global.gc(); + } + const memAfterDump = process.memoryUsage().heapUsed; + + // Override and measure memory impact + const overrideStart = performance.now(); + const newStore = haro(); + newStore.override(recordsDump, "records"); + newStore.override(indexesDump, "indexes"); + const overrideEnd = performance.now(); + + if (global.gc) { + global.gc(); + } + const memAfterOverride = process.memoryUsage().heapUsed; + + // Cleanup dumps and measure memory recovery + // (In real usage, dumps would be serialized and stored externally) + const memAfterCleanup = process.memoryUsage().heapUsed; + + results.push({ + name: `Memory efficiency analysis (${size} records)`, + iterations: 1, + totalTime: dumpEnd - dumpStart + (overrideEnd - overrideStart), + dumpTime: dumpEnd - dumpStart, + overrideTime: overrideEnd - overrideStart, + originalMemory: (memAfterCreate - memBefore) / 1024 / 1024, // MB + dumpMemoryImpact: (memAfterDump - memAfterCreate) / 1024 / 1024, // MB + overrideMemoryImpact: (memAfterOverride - memAfterDump) / 1024 / 1024, // MB + finalMemory: (memAfterCleanup - memBefore) / 1024 / 1024, // MB + opsPerSecond: Math.floor(1000 / (dumpEnd - dumpStart + (overrideEnd - overrideStart))) + }); + }); + + return results; +} + +/** + * Benchmarks persistence with large complex objects + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkComplexObjectPersistence (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + // Generate more complex test data + const complexData = []; + for (let i = 0; i < size; i++) { + complexData.push({ + id: i, + profile: { + personal: { + name: `User ${i}`, + email: `user${i}@test.com`, + birth: new Date(1990 + i % 30, i % 12, i % 28 + 1) + }, + professional: { + title: `Title ${i % 20}`, + department: `Dept ${i % 10}`, + experience: Array.from({ length: i % 5 + 1 }, (_, j) => ({ + company: `Company ${j}`, + role: `Role ${j}`, + duration: `${j + 1} years` + })) + } + }, + activities: Array.from({ length: i % 50 + 1 }, (_, j) => ({ + id: `activity_${i}_${j}`, + type: `type_${j % 10}`, + timestamp: new Date(Date.now() - j * 86400000), + data: { + action: `action_${j}`, + details: { value: Math.random() * 1000, category: `cat_${j % 5}` } + } + })), + settings: { + preferences: Object.fromEntries( + Array.from({ length: 20 }, (_, j) => [`pref_${j}`, Math.random() > 0.5]) + ), + permissions: Array.from({ length: 10 }, (_, j) => `perm_${j}`) + } + }); + } + + const store = haro(complexData, { + index: ["profile.professional.department", "settings.permissions"] + }); + + // Dump complex objects + const dumpComplexResult = benchmark(`DUMP complex objects (${size} records)`, () => { + return store.dump("records"); + }); + results.push(dumpComplexResult); + + // Override complex objects + const dump = store.dump("records"); + const overrideComplexResult = benchmark(`OVERRIDE complex objects (${size} records)`, () => { + const targetStore = haro(); + targetStore.override(dump, "records"); + + return targetStore; + }); + results.push(overrideComplexResult); + + // Analyze data complexity impact + const dataComplexityResult = { + name: `Complex object analysis (${size} records)`, + iterations: 1, + totalTime: 0, + avgTime: 0, + opsPerSecond: 0, + averageObjectSize: JSON.stringify(complexData[0]).length, + totalDataSize: JSON.stringify(dump).length, + compressionRatio: JSON.stringify(dump).length / JSON.stringify(complexData).length + }; + results.push(dataComplexityResult); + }); + + return results; +} + +/** + * Prints formatted benchmark results + * @param {Array} results - Array of benchmark results + */ +function printResults (results) { + console.log("\n" + "=".repeat(80)); + console.log("PERSISTENCE BENCHMARK RESULTS"); + console.log("=".repeat(80)); + + results.forEach(result => { + const opsIndicator = result.opsPerSecond > 100 ? "✅" : + result.opsPerSecond > 10 ? "🟡" : + result.opsPerSecond > 1 ? "🟠" : "🔴"; + + if (result.opsPerSecond > 0) { + console.log(`${opsIndicator} ${result.name}`); + console.log(` ${result.opsPerSecond.toLocaleString()} ops/sec | ${result.totalTime.toFixed(2)}ms total | ${result.avgTime?.toFixed(4) || "N/A"}ms avg`); + } else { + console.log(`📊 ${result.name}`); + } + + // Special formatting for different result types + if (result.recordsSize !== undefined) { + console.log(` Records: ${result.recordsSize} items, ${(result.recordsDataSize / 1024).toFixed(2)}KB`); + console.log(` Indexes: ${result.indexesSize} items, ${(result.indexesDataSize / 1024).toFixed(2)}KB`); + } + + if (result.integrityMatch !== undefined) { + console.log(` Original: ${result.originalSize} | Restored: ${result.restoredSize}`); + console.log(` Integrity: ${result.integrityMatch ? "✅" : "❌"} | Sample match: ${result.sampleRecordMatch ? "✅" : "❌"}`); + } + + if (result.originalMemory !== undefined) { + console.log(` Dump: ${result.dumpTime.toFixed(2)}ms | Override: ${result.overrideTime.toFixed(2)}ms`); + console.log(` Memory - Original: ${result.originalMemory.toFixed(2)}MB | Final: ${result.finalMemory.toFixed(2)}MB`); + console.log(` Memory Impact - Dump: ${result.dumpMemoryImpact.toFixed(2)}MB | Override: ${result.overrideMemoryImpact.toFixed(2)}MB`); + } + + if (result.averageObjectSize !== undefined) { + console.log(` Avg object: ${result.averageObjectSize} bytes | Total: ${(result.totalDataSize / 1024).toFixed(2)}KB`); + console.log(` Compression ratio: ${(result.compressionRatio * 100).toFixed(1)}%`); + } + + console.log(""); + }); +} + +/** + * Runs all persistence benchmarks + * @returns {Array} Array of all benchmark results + */ +function runPersistenceBenchmarks () { + console.log("Starting Persistence Benchmarks...\n"); + + const dataSizes = [100, 1000, 5000]; + let allResults = []; + + console.log("Testing dump operations..."); + allResults.push(...benchmarkDumpOperations(dataSizes)); + + console.log("Testing override operations..."); + allResults.push(...benchmarkOverrideOperations(dataSizes)); + + console.log("Testing round-trip persistence..."); + allResults.push(...benchmarkRoundTripPersistence(dataSizes)); + + console.log("Testing persistence memory efficiency..."); + allResults.push(...benchmarkPersistenceMemory([100, 1000])); // Smaller sizes for memory tests + + console.log("Testing complex object persistence..."); + allResults.push(...benchmarkComplexObjectPersistence([50, 500])); // Smaller sizes for complex data + + printResults(allResults); + + console.log("Persistence Benchmarks completed.\n"); + + return allResults; +} + +// Export for use in main benchmark runner +export { runPersistenceBenchmarks }; + +// Run standalone if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runPersistenceBenchmarks(); +} diff --git a/benchmarks/search-filter.js b/benchmarks/search-filter.js new file mode 100644 index 00000000..b015a205 --- /dev/null +++ b/benchmarks/search-filter.js @@ -0,0 +1,479 @@ +import { performance } from "node:perf_hooks"; +import { haro } from "../dist/haro.js"; + +/** + * Generates test data with structured fields for search benchmarking + * @param {number} size - Number of records to generate + * @returns {Array} Array of test records with searchable fields + */ +function generateSearchTestData (size) { + const data = []; + const departments = ["Engineering", "Marketing", "Sales", "HR", "Finance"]; + const skills = ["JavaScript", "Python", "Java", "React", "Node.js", "SQL", "Docker", "AWS"]; + const cities = ["New York", "San Francisco", "Boston", "Austin", "Seattle"]; + + for (let i = 0; i < size; i++) { + data.push({ + id: i, + name: `User ${i}`, + email: `user${i}@example.com`, + age: Math.floor(Math.random() * 50) + 18, + department: departments[i % departments.length], + skills: [ + skills[i % skills.length], + skills[(i + 1) % skills.length], + skills[(i + 2) % skills.length] + ], + city: cities[i % cities.length], + active: Math.random() > 0.3, + salary: Math.floor(Math.random() * 100000) + 50000, + joinDate: new Date(2020 + Math.floor(Math.random() * 4), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28)), + tags: [`tag${i % 10}`, `category${i % 5}`], + metadata: { + created: new Date(), + score: Math.random() * 100, + level: Math.floor(Math.random() * 10), + region: `Region ${i % 3}` + } + }); + } + + return data; +} + +/** + * Runs a benchmark test and returns timing information + * @param {string} name - Name of the test + * @param {Function} fn - Function to benchmark + * @param {number} iterations - Number of iterations to run + * @returns {Object} Benchmark results + */ +function benchmark (name, fn, iterations = 1000) { + const start = performance.now(); + for (let i = 0; i < iterations; i++) { + fn(); + } + const end = performance.now(); + const total = end - start; + const avgTime = total / iterations; + + return { + name, + iterations, + totalTime: total, + avgTime, + opsPerSecond: Math.floor(1000 / avgTime) + }; +} + +/** + * Benchmarks FIND operations using indexes + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkFindOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateSearchTestData(size); + const store = haro(testData, { + index: ["department", "age", "city", "active", "active|department", "city|department"] + }); + + // Simple find operations + const findDeptResult = benchmark(`FIND by department (${size} records)`, () => { + store.find({ department: "Engineering" }); + }); + results.push(findDeptResult); + + const findActiveResult = benchmark(`FIND by active status (${size} records)`, () => { + store.find({ active: true }); + }); + results.push(findActiveResult); + + // Composite find operations + const findCompositeResult = benchmark(`FIND by department+active (${size} records)`, () => { + store.find({ department: "Engineering", active: true }); + }); + results.push(findCompositeResult); + + const findCityDeptResult = benchmark(`FIND by city+department (${size} records)`, () => { + store.find({ city: "New York", department: "Engineering" }); + }); + results.push(findCityDeptResult); + }); + + return results; +} + +/** + * Benchmarks FILTER operations using predicates + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkFilterOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateSearchTestData(size); + const store = haro(testData); + + // Simple filter operations + const filterAgeResult = benchmark(`FILTER by age range (${size} records)`, () => { + store.filter(record => record.age >= 25 && record.age <= 35); + }); + results.push(filterAgeResult); + + const filterSalaryResult = benchmark(`FILTER by salary range (${size} records)`, () => { + store.filter(record => record.salary > 75000); + }); + results.push(filterSalaryResult); + + // Complex filter operations + const filterComplexResult = benchmark(`FILTER complex condition (${size} records)`, () => { + store.filter(record => + record.active && + record.age > 30 && + record.department === "Engineering" && + record.skills.includes("JavaScript") + ); + }); + results.push(filterComplexResult); + + // Array filter operations + const filterArrayResult = benchmark(`FILTER by array contains (${size} records)`, () => { + store.filter(record => record.skills.includes("React")); + }); + results.push(filterArrayResult); + }); + + return results; +} + +/** + * Benchmarks SEARCH operations using different value types + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkSearchOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateSearchTestData(size); + const store = haro(testData, { + index: ["department", "skills", "city", "name", "tags"] + }); + + // String search operations + const searchStringResult = benchmark(`SEARCH by string value (${size} records)`, () => { + store.search("Engineering", "department"); + }); + results.push(searchStringResult); + + // Regex search operations + const searchRegexResult = benchmark(`SEARCH by regex (${size} records)`, () => { + store.search(/^User [0-9]+$/, "name"); + }); + results.push(searchRegexResult); + + // Function search operations + const searchFunctionResult = benchmark(`SEARCH by function (${size} records)`, () => { + store.search((value, index) => { + if (index === "department") { + return value.startsWith("Eng"); + } + + return false; + }); + }); + results.push(searchFunctionResult); + + // Multiple index search + const searchMultipleResult = benchmark(`SEARCH multiple indexes (${size} records)`, () => { + store.search("tag1", ["tags", "skills"]); + }); + results.push(searchMultipleResult); + }); + + return results; +} + +/** + * Benchmarks WHERE operations with different operators and complex predicates + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkWhereOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateSearchTestData(size); + const store = haro(testData, { + index: ["department", "skills", "city", "active", "tags", "age", "salary", "department|active", "city|department"] + }); + + // Simple where operations + const whereDeptResult = benchmark(`WHERE by department (${size} records)`, () => { + store.where({ department: "Engineering" }); + }); + results.push(whereDeptResult); + + // Array where operations with OR (default) + const whereArrayOrResult = benchmark(`WHERE array OR operation (${size} records)`, () => { + store.where({ + skills: ["JavaScript", "Python"] + }, "||"); + }); + results.push(whereArrayOrResult); + + // Array where operations with AND + const whereArrayAndResult = benchmark(`WHERE array AND operation (${size} records)`, () => { + store.where({ + skills: ["JavaScript", "React"] + }, "&&"); + }); + results.push(whereArrayAndResult); + + // Multiple array fields with OR + const whereMultiArrayOrResult = benchmark(`WHERE multiple arrays OR (${size} records)`, () => { + store.where({ + skills: ["JavaScript", "Python"], + tags: ["tag0", "tag1"] + }, "||"); + }); + results.push(whereMultiArrayOrResult); + + // Multiple array fields with AND + const whereMultiArrayAndResult = benchmark(`WHERE multiple arrays AND (${size} records)`, () => { + store.where({ + skills: ["JavaScript"], + tags: ["tag0"] + }, "&&"); + }); + results.push(whereMultiArrayAndResult); + + // Regex where operations + const whereRegexResult = benchmark(`WHERE with regex (${size} records)`, () => { + store.where({ + department: /^Eng/ + }); + }); + results.push(whereRegexResult); + + // Multiple regex patterns + const whereMultiRegexResult = benchmark(`WHERE multiple regex (${size} records)`, () => { + store.where({ + department: /^(Engineering|Marketing)$/, + city: /^(New|San)/ + }); + }); + results.push(whereMultiRegexResult); + + // Complex where operations with mixed types + const whereComplexResult = benchmark(`WHERE complex mixed conditions (${size} records)`, () => { + store.where({ + department: "Engineering", + active: true, + skills: ["JavaScript"] + }); + }); + results.push(whereComplexResult); + + // Very complex where with all predicate types + const whereVeryComplexResult = benchmark(`WHERE very complex predicates (${size} records)`, () => { + store.where({ + department: ["Engineering", "Marketing"], + active: true, + skills: ["JavaScript", "Python"], + city: /^(New|San)/ + }, "||"); + }); + results.push(whereVeryComplexResult); + + // Nested field matching (if metadata fields are indexed) + const whereNestedResult = benchmark(`WHERE nested field matching (${size} records)`, () => { + store.where({ + department: "Engineering", + tags: ["tag0", "tag1"] + }); + }); + results.push(whereNestedResult); + + // Performance comparison: where vs filter for same conditions + const whereVsFilterResult = benchmark(`WHERE vs FILTER comparison (${size} records)`, () => { + const wherePredicate = { department: "Engineering", active: true }; + const whereResults = store.where(wherePredicate); + const filterResults = store.filter(record => + record.department === "Engineering" && record.active === true + ); + + return { whereCount: whereResults.length, filterCount: filterResults.length }; + }); + results.push(whereVsFilterResult); + + // Edge case: empty predicate + const whereEmptyResult = benchmark(`WHERE empty predicate (${size} records)`, () => { + store.where({}); + }); + results.push(whereEmptyResult); + + // Edge case: non-indexed field + const whereNonIndexedResult = benchmark(`WHERE non-indexed field (${size} records)`, () => { + store.where({ + email: "user0@example.com" + }); + }); + results.push(whereNonIndexedResult); + }); + + return results; +} + +/** + * Benchmarks MAP and REDUCE operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkMapReduceOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateSearchTestData(size); + const store = haro(testData); + + // Map operations + const mapResult = benchmark(`MAP transformation (${size} records)`, () => { + store.map(record => ({ + id: record.id, + name: record.name, + department: record.department + })); + }); + results.push(mapResult); + + // Reduce operations + const reduceResult = benchmark(`REDUCE aggregation (${size} records)`, () => { + store.reduce((acc, record) => { + acc[record.department] = (acc[record.department] || 0) + 1; + + return acc; + }, {}); + }); + results.push(reduceResult); + + // ForEach operations + const forEachResult = benchmark(`FOREACH iteration (${size} records)`, () => { + let count = 0; // eslint-disable-line no-unused-vars + store.forEach(() => { + count++; + }); + }); + results.push(forEachResult); + }); + + return results; +} + +/** + * Benchmarks SORT operations + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkSortOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateSearchTestData(size); + const store = haro(testData, { + index: ["age", "salary", "name", "department"] + }); + + // Sort operations + const sortResult = benchmark(`SORT by age (${size} records)`, () => { + store.sort((a, b) => a.age - b.age); + }); + results.push(sortResult); + + // SortBy operations + const sortByResult = benchmark(`SORT BY indexed field (${size} records)`, () => { + store.sortBy("age"); + }); + results.push(sortByResult); + + // Complex sort operations + const complexSortResult = benchmark(`SORT complex comparison (${size} records)`, () => { + store.sort((a, b) => { + if (a.department !== b.department) { + return a.department.localeCompare(b.department); + } + + return b.salary - a.salary; + }); + }); + results.push(complexSortResult); + }); + + return results; +} + +/** + * Prints benchmark results in a formatted table + * @param {Array} results - Array of benchmark results + */ +function printResults (results) { + console.log("\n=== SEARCH & FILTER BENCHMARK RESULTS ===\n"); + + console.log("Operation".padEnd(40) + "Iterations".padEnd(12) + "Total Time (ms)".padEnd(18) + "Avg Time (ms)".padEnd(16) + "Ops/Second"); + console.log("-".repeat(98)); + + results.forEach(result => { + const name = result.name.padEnd(40); + const iterations = result.iterations.toString().padEnd(12); + const totalTime = result.totalTime.toFixed(2).padEnd(18); + const avgTime = result.avgTime.toFixed(4).padEnd(16); + const opsPerSecond = result.opsPerSecond.toLocaleString(); + + console.log(name + iterations + totalTime + avgTime + opsPerSecond); + }); + + console.log("\n"); +} + +/** + * Main function to run all search and filter benchmarks + */ +function runSearchFilterBenchmarks () { + console.log("🔍 Running Search & Filter Benchmarks...\n"); + + const dataSizes = [1000, 10000, 50000]; + const allResults = []; + + console.log("Testing FIND operations..."); + allResults.push(...benchmarkFindOperations(dataSizes)); + + console.log("Testing FILTER operations..."); + allResults.push(...benchmarkFilterOperations(dataSizes)); + + console.log("Testing SEARCH operations..."); + allResults.push(...benchmarkSearchOperations(dataSizes)); + + console.log("Testing WHERE operations..."); + allResults.push(...benchmarkWhereOperations(dataSizes)); + + console.log("Testing MAP/REDUCE operations..."); + allResults.push(...benchmarkMapReduceOperations(dataSizes)); + + console.log("Testing SORT operations..."); + allResults.push(...benchmarkSortOperations(dataSizes)); + + printResults(allResults); + + return allResults; +} + +// Run benchmarks if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runSearchFilterBenchmarks(); +} + +export { runSearchFilterBenchmarks, generateSearchTestData }; diff --git a/benchmarks/utility-operations.js b/benchmarks/utility-operations.js new file mode 100644 index 00000000..88a2a792 --- /dev/null +++ b/benchmarks/utility-operations.js @@ -0,0 +1,347 @@ +import { performance } from "node:perf_hooks"; +import { haro } from "../dist/haro.js"; + +/** + * Generates test data for utility operation benchmarking + * @param {number} size - Number of records to generate + * @returns {Array} Array of test records with complex nested structures + */ +function generateUtilityTestData (size) { + const data = []; + for (let i = 0; i < size; i++) { + data.push({ + id: i, + name: `User ${i}`, + email: `user${i}@example.com`, + age: Math.floor(Math.random() * 50) + 18, + tags: [`tag${i % 10}`, `category${i % 5}`, `type${i % 3}`], + metadata: { + created: new Date(), + score: Math.random() * 100, + level: Math.floor(Math.random() * 10), + preferences: { + theme: i % 2 === 0 ? "dark" : "light", + notifications: Math.random() > 0.5, + settings: { + privacy: Math.random() > 0.3, + analytics: Math.random() > 0.7 + } + } + }, + history: Array.from({ length: Math.min(i % 20 + 1, 10) }, (_, j) => ({ + action: `action_${j}`, + timestamp: new Date(Date.now() - j * 1000 * 60), + data: { value: Math.random() * 1000 } + })) + }); + } + + return data; +} + +/** + * Runs a benchmark test and returns timing information + * @param {string} name - Name of the test + * @param {Function} fn - Function to benchmark + * @param {number} iterations - Number of iterations to run + * @returns {Object} Benchmark results + */ +function benchmark (name, fn, iterations = 1000) { + const start = performance.now(); + for (let i = 0; i < iterations; i++) { + fn(); + } + const end = performance.now(); + const total = end - start; + const avgTime = total / iterations; + + return { + name, + iterations, + totalTime: total, + avgTime, + opsPerSecond: Math.floor(1000 / avgTime) + }; +} + +/** + * Benchmarks clone operations on various data structures + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkCloneOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateUtilityTestData(size); + const store = haro(testData); + + // Clone simple objects + const simpleObject = { id: 1, name: "test", age: 30 }; + const cloneSimpleResult = benchmark(`Clone simple object (${size} iterations)`, () => { + store.clone(simpleObject); + }); + results.push(cloneSimpleResult); + + // Clone complex nested objects + const complexObject = testData[0]; + const cloneComplexResult = benchmark(`Clone complex object (${size} iterations)`, () => { + store.clone(complexObject); + }); + results.push(cloneComplexResult); + + // Clone arrays + const arrayData = testData.slice(0, Math.min(100, size)); + const cloneArrayResult = benchmark(`Clone array (${arrayData.length} items, ${Math.min(100, size)} iterations)`, () => { + store.clone(arrayData); + }, Math.min(100, size)); + results.push(cloneArrayResult); + }); + + return results; +} + +/** + * Benchmarks merge operations with different data structures + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkMergeOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const store = haro(); + + // Merge simple objects + const base = { id: 1, name: "John", age: 30 }; + const update = { age: 31, email: "john@example.com" }; + const mergeSimpleResult = benchmark(`Merge simple objects (${size} iterations)`, () => { + store.merge(store.clone(base), update); + }); + results.push(mergeSimpleResult); + + // Merge complex nested objects + const complexBase = { + id: 1, + profile: { name: "John", age: 30 }, + settings: { theme: "dark", notifications: true }, + tags: ["user", "admin"] + }; + const complexUpdate = { + profile: { age: 31, location: "NYC" }, + settings: { privacy: true }, + tags: ["power-user"] + }; + const mergeComplexResult = benchmark(`Merge complex objects (${size} iterations)`, () => { + store.merge(store.clone(complexBase), complexUpdate); + }); + results.push(mergeComplexResult); + + // Merge arrays + const array1 = Array.from({ length: 10 }, (_, i) => i); + const array2 = Array.from({ length: 10 }, (_, i) => i + 10); + const mergeArrayResult = benchmark(`Merge arrays (${size} iterations)`, () => { + store.merge(store.clone(array1), array2); + }); + results.push(mergeArrayResult); + + // Merge with override + const mergeOverrideResult = benchmark(`Merge with override (${size} iterations)`, () => { + store.merge(store.clone(array1), array2, true); + }); + results.push(mergeOverrideResult); + }); + + return results; +} + +/** + * Benchmarks freeze operations on various data structures + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkFreezeOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateUtilityTestData(size); + const store = haro(); + + // Freeze single objects + const singleObject = testData[0]; + const freezeSingleResult = benchmark(`Freeze single object (${size} iterations)`, () => { + store.freeze(singleObject); + }); + results.push(freezeSingleResult); + + // Freeze multiple objects + const multipleObjects = testData.slice(0, Math.min(10, size)); + const freezeMultipleResult = benchmark(`Freeze multiple objects (${multipleObjects.length} objects, ${Math.min(100, size)} iterations)`, () => { + store.freeze(...multipleObjects); + }, Math.min(100, size)); + results.push(freezeMultipleResult); + + // Freeze nested structures + const nestedStructure = { + data: testData.slice(0, Math.min(50, size)), + metadata: { count: size, timestamp: new Date() } + }; + const freezeNestedResult = benchmark(`Freeze nested structure (${Math.min(10, size)} iterations)`, () => { + store.freeze(nestedStructure); + }, Math.min(10, size)); + results.push(freezeNestedResult); + }); + + return results; +} + +/** + * Benchmarks forEach operations with different callback complexities + * @param {Array} dataSizes - Array of data sizes to test + * @returns {Array} Array of benchmark results + */ +function benchmarkForEachOperations (dataSizes) { + const results = []; + + dataSizes.forEach(size => { + const testData = generateUtilityTestData(size); + const store = haro(testData); + + // Simple forEach operation + const forEachSimpleResult = benchmark(`forEach simple operation (${size} records)`, () => { + let count = 0; // eslint-disable-line no-unused-vars + store.forEach(() => { count++; }); + }, 1); + results.push(forEachSimpleResult); + + // Complex forEach operation + const aggregated = {}; + const forEachComplexResult = benchmark(`forEach complex operation (${size} records)`, () => { + store.forEach(record => { + const dept = record.metadata?.preferences?.theme || "unknown"; + aggregated[dept] = (aggregated[dept] || 0) + 1; + }); + }, 1); + results.push(forEachComplexResult); + + // forEach with context + const context = { processed: 0, errors: 0 }; + const forEachContextResult = benchmark(`forEach with context (${size} records)`, () => { + store.forEach(function (record) { + try { + if (record.age > 0) { + this.processed++; + } + } catch (e) { // eslint-disable-line no-unused-vars + this.errors++; + } + }, context); + }, 1); + results.push(forEachContextResult); + }); + + return results; +} + +/** + * Benchmarks UUID generation performance + * @param {Array} iterations - Array of iteration counts to test + * @returns {Array} Array of benchmark results + */ +function benchmarkUuidOperations (iterations) { + const results = []; + const store = haro(); + + iterations.forEach(count => { + // UUID generation + const uuidResult = benchmark(`UUID generation (${count} iterations)`, () => { + store.uuid(); + }, count); + results.push(uuidResult); + + // UUID uniqueness test (collect UUIDs and check for duplicates) + const uuids = new Set(); + const uniquenessStart = performance.now(); + for (let i = 0; i < count; i++) { + uuids.add(store.uuid()); + } + const uniquenessEnd = performance.now(); + const uniquenessResult = { + name: `UUID uniqueness test (${count} UUIDs)`, + iterations: count, + totalTime: uniquenessEnd - uniquenessStart, + avgTime: (uniquenessEnd - uniquenessStart) / count, + opsPerSecond: Math.floor(count / ((uniquenessEnd - uniquenessStart) / 1000)), + duplicates: count - uuids.size, + uniqueRatio: (uuids.size / count * 100).toFixed(2) + "%" + }; + results.push(uniquenessResult); + }); + + return results; +} + +/** + * Prints formatted benchmark results + * @param {Array} results - Array of benchmark results + */ +function printResults (results) { + console.log("\n" + "=".repeat(80)); + console.log("UTILITY OPERATIONS BENCHMARK RESULTS"); + console.log("=".repeat(80)); + + results.forEach(result => { + const opsIndicator = result.opsPerSecond > 10000 ? "✅" : + result.opsPerSecond > 1000 ? "🟡" : + result.opsPerSecond > 100 ? "🟠" : "🔴"; + + console.log(`${opsIndicator} ${result.name}`); + console.log(` ${result.opsPerSecond.toLocaleString()} ops/sec | ${result.totalTime.toFixed(2)}ms total | ${result.avgTime.toFixed(4)}ms avg`); + + if (result.duplicates !== undefined) { + console.log(` Duplicates: ${result.duplicates} | Unique ratio: ${result.uniqueRatio}`); + } + console.log(""); + }); +} + +/** + * Runs all utility operation benchmarks + * @returns {Array} Array of all benchmark results + */ +function runUtilityOperationsBenchmarks () { + console.log("Starting Utility Operations Benchmarks...\n"); + + const dataSizes = [100, 1000, 5000]; + const uuidIterations = [1000, 10000, 50000]; + let allResults = []; + + console.log("Testing clone operations..."); + allResults.push(...benchmarkCloneOperations(dataSizes)); + + console.log("Testing merge operations..."); + allResults.push(...benchmarkMergeOperations(dataSizes)); + + console.log("Testing freeze operations..."); + allResults.push(...benchmarkFreezeOperations(dataSizes)); + + console.log("Testing forEach operations..."); + allResults.push(...benchmarkForEachOperations(dataSizes)); + + console.log("Testing UUID operations..."); + allResults.push(...benchmarkUuidOperations(uuidIterations)); + + printResults(allResults); + + console.log("Utility Operations Benchmarks completed.\n"); + + return allResults; +} + +// Export for use in main benchmark runner +export { runUtilityOperationsBenchmarks }; + +// Run standalone if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + runUtilityOperationsBenchmarks(); +} diff --git a/dist/haro.cjs b/dist/haro.cjs index 04792060..342df23f 100644 --- a/dist/haro.cjs +++ b/dist/haro.cjs @@ -3,62 +3,85 @@ * * @copyright 2025 Jason Mulligan * @license BSD-3-Clause - * @version 15.2.6 + * @version 16.0.0 */ 'use strict'; +var crypto = require('crypto'); + +// String constants - Single characters and symbols const STRING_COMMA = ","; const STRING_EMPTY = ""; const STRING_PIPE = "|"; const STRING_DOUBLE_PIPE = "||"; -const STRING_A = "a"; -const STRING_B = "b"; +const STRING_DOUBLE_AND = "&&"; + +// String constants - Operation and type names +const STRING_ID = "id"; const STRING_DEL = "del"; const STRING_FUNCTION = "function"; const STRING_INDEXES = "indexes"; -const STRING_INVALID_FIELD = "Invalid field"; -const STRING_INVALID_FUNCTION = "Invalid function"; -const STRING_INVALID_TYPE = "Invalid type"; const STRING_OBJECT = "object"; -const STRING_RECORD_NOT_FOUND = "Record not found"; const STRING_RECORDS = "records"; const STRING_REGISTRY = "registry"; const STRING_SET = "set"; const STRING_SIZE = "size"; -const INT_0 = 0; -const INT_1 = 1; -const INT_3 = 3; -const INT_4 = 4; -const INT_8 = 8; -const INT_9 = 9; -const INT_16 = 16; - -/* istanbul ignore next */ -const r = [INT_8, INT_9, STRING_A, STRING_B]; - -/* istanbul ignore next */ -function s () { - return ((Math.random() + INT_1) * 0x10000 | INT_0).toString(INT_16).substring(INT_1); -} +const STRING_STRING = "string"; +const STRING_NUMBER = "number"; -/* istanbul ignore next */ -function randomUUID () { - return `${s()}${s()}-${s()}-4${s().slice(INT_0, INT_3)}-${r[Math.floor(Math.random() * INT_4)]}${s().slice(INT_0, INT_3)}-${s()}${s()}${s()}`; -} +// String constants - Error messages +const STRING_INVALID_FIELD = "Invalid field"; +const STRING_INVALID_FUNCTION = "Invalid function"; +const STRING_INVALID_TYPE = "Invalid type"; +const STRING_RECORD_NOT_FOUND = "Record not found"; -const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : randomUUID; +// Integer constants +const INT_0 = 0; +/** + * Haro is a modern immutable DataStore for collections of records with indexing, + * versioning, and batch operations support. It provides a Map-like interface + * with advanced querying capabilities through indexes. + * @class + * @example + * const store = new Haro({ + * index: ['name', 'age'], + * key: 'id', + * versioning: true + * }); + * + * store.set(null, {name: 'John', age: 30}); + * const results = store.find({name: 'John'}); + */ class Haro { - constructor ({delimiter = STRING_PIPE, id = this.uuid(), index = [], key = "id", versioning = false} = {}) { + /** + * Creates a new Haro instance with specified configuration + * @param {Object} [config={}] - Configuration object for the store + * @param {string} [config.delimiter=STRING_PIPE] - Delimiter for composite indexes (default: '|') + * @param {string} [config.id] - Unique identifier for this instance (auto-generated if not provided) + * @param {boolean} [config.immutable=false] - Return frozen/immutable objects for data safety + * @param {string[]} [config.index=[]] - Array of field names to create indexes for + * @param {string} [config.key=STRING_ID] - Primary key field name used for record identification + * @param {boolean} [config.versioning=false] - Enable versioning to track record changes + * @constructor + * @example + * const store = new Haro({ + * index: ['name', 'email', 'name|department'], + * key: 'userId', + * versioning: true, + * immutable: true + * }); + */ + constructor ({delimiter = STRING_PIPE, id = this.uuid(), immutable = false, index = [], key = STRING_ID, versioning = false} = {}) { this.data = new Map(); this.delimiter = delimiter; this.id = id; + this.immutable = immutable; this.index = Array.isArray(index) ? [...index] : []; this.indexes = new Map(); this.key = key; this.versions = new Map(); this.versioning = versioning; - Object.defineProperty(this, STRING_REGISTRY, { enumerable: true, get: () => Array.from(this.data.keys()) @@ -71,28 +94,78 @@ class Haro { return this.reindex(); } + /** + * Performs batch operations on multiple records for efficient bulk processing + * @param {Array} args - Array of records to process + * @param {string} [type=STRING_SET] - Type of operation: 'set' for upsert, 'del' for delete + * @returns {Array} Array of results from the batch operation + * @throws {Error} Throws error if individual operations fail during batch processing + * @example + * const results = store.batch([ + * {id: 1, name: 'John'}, + * {id: 2, name: 'Jane'} + * ], 'set'); + */ batch (args, type = STRING_SET) { - const fn = type === STRING_DEL ? i => this.del(i, true) : i => this.set(null, i, true, true); + const fn = type === STRING_DEL ? i => this.delete(i, true) : i => this.set(null, i, true, true); return this.onbatch(this.beforeBatch(args, type).map(fn), type); } + /** + * Lifecycle hook executed before batch operations for custom preprocessing + * @param {Array} arg - Arguments passed to batch operation + * @param {string} [type=STRING_EMPTY] - Type of batch operation ('set' or 'del') + * @returns {Array} The arguments array (possibly modified) to be processed + */ beforeBatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars + // Hook for custom logic before batch; override in subclass if needed return arg; } + /** + * Lifecycle hook executed before clear operation for custom preprocessing + * @returns {void} Override this method in subclasses to implement custom logic + * @example + * class MyStore extends Haro { + * beforeClear() { + * this.backup = this.toArray(); + * } + * } + */ beforeClear () { // Hook for custom logic before clear; override in subclass if needed } - beforeDelete (key = STRING_EMPTY, batch = false) { - return [key, batch]; + /** + * Lifecycle hook executed before delete operation for custom preprocessing + * @param {string} [key=STRING_EMPTY] - Key of record to delete + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + beforeDelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic before delete; override in subclass if needed } - beforeSet (key = STRING_EMPTY, batch = false) { - return [key, batch]; + /** + * Lifecycle hook executed before set operation for custom preprocessing + * @param {string} [key=STRING_EMPTY] - Key of record to set + * @param {Object} [data={}] - Record data being set + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @param {boolean} [override=false] - Whether to override existing data + * @returns {void} Override this method in subclasses to implement custom logic + */ + beforeSet (key = STRING_EMPTY, data = {}, batch = false, override = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic before set; override in subclass if needed } + /** + * Removes all records, indexes, and versions from the store + * @returns {Haro} This instance for method chaining + * @example + * store.clear(); + * console.log(store.size); // 0 + */ clear () { this.beforeClear(); this.data.clear(); @@ -103,17 +176,36 @@ class Haro { return this; } + /** + * Creates a deep clone of the given value, handling objects, arrays, and primitives + * @param {*} arg - Value to clone (any type) + * @returns {*} Deep clone of the argument + * @example + * const original = {name: 'John', tags: ['user', 'admin']}; + * const cloned = store.clone(original); + * cloned.tags.push('new'); // original.tags is unchanged + */ clone (arg) { - return JSON.parse(JSON.stringify(arg)); + return structuredClone(arg); } - del (key = STRING_EMPTY, batch = false) { + /** + * Deletes a record from the store and removes it from all indexes + * @param {string} [key=STRING_EMPTY] - Key of record to delete + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @returns {void} + * @throws {Error} Throws error if record with the specified key is not found + * @example + * store.delete('user123'); + * // Throws error if 'user123' doesn't exist + */ + delete (key = STRING_EMPTY, batch = false) { if (!this.data.has(key)) { throw new Error(STRING_RECORD_NOT_FOUND); } const og = this.get(key, true); this.beforeDelete(key, batch); - this.delIndex(this.index, this.indexes, this.delimiter, key, og); + this.deleteIndex(key, og); this.data.delete(key); this.ondelete(key, batch); if (this.versioning) { @@ -121,12 +213,18 @@ class Haro { } } - delIndex (index, indexes, delimiter, key, data) { - index.forEach(i => { - const idx = indexes.get(i); + /** + * Internal method to remove entries from indexes for a deleted record + * @param {string} key - Key of record being deleted + * @param {Object} data - Data of record being deleted + * @returns {Haro} This instance for method chaining + */ + deleteIndex (key, data) { + this.index.forEach(i => { + const idx = this.indexes.get(i); if (!idx) return; - const values = i.includes(delimiter) ? - this.indexKeys(i, delimiter, data) : + const values = i.includes(this.delimiter) ? + this.indexKeys(i, this.delimiter, data) : Array.isArray(data[i]) ? data[i] : [data[i]]; this.each(values, value => { if (idx.has(value)) { @@ -138,11 +236,20 @@ class Haro { } }); }); + + return this; } + /** + * Exports complete store data or indexes for persistence or debugging + * @param {string} [type=STRING_RECORDS] - Type of data to export: 'records' or 'indexes' + * @returns {Array} Array of [key, value] pairs for records, or serialized index structure + * @example + * const records = store.dump('records'); + * const indexes = store.dump('indexes'); + */ dump (type = STRING_RECORDS) { let result; - if (type === STRING_RECORDS) { result = Array.from(this.entries()); } else { @@ -160,20 +267,46 @@ class Haro { return result; } + /** + * Utility method to iterate over an array with a callback function + * @param {Array<*>} [arr=[]] - Array to iterate over + * @param {Function} fn - Function to call for each element (element, index) + * @returns {Array<*>} The original array for method chaining + * @example + * store.each([1, 2, 3], (item, index) => console.log(item, index)); + */ each (arr = [], fn) { - for (const [idx, value] of arr.entries()) { - fn(value, idx); + const len = arr.length; + for (let i = 0; i < len; i++) { + fn(arr[i], i); } return arr; } + /** + * Returns an iterator of [key, value] pairs for each record in the store + * @returns {Iterator>} Iterator of [key, value] pairs + * @example + * for (const [key, value] of store.entries()) { + * console.log(key, value); + * } + */ entries () { return this.data.entries(); } + /** + * Finds records matching the specified criteria using indexes for optimal performance + * @param {Object} [where={}] - Object with field-value pairs to match against + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of matching records (frozen if immutable mode) + * @example + * const users = store.find({department: 'engineering', active: true}); + * const admins = store.find({role: 'admin'}); + */ find (where = {}, raw = false) { - const key = Object.keys(where).sort((a, b) => a.localeCompare(b)).join(this.delimiter); + const key = Object.keys(where).sort(this.sortKeys).join(this.delimiter); const index = this.indexes.get(key) ?? new Map(); let result = []; if (index.size > 0) { @@ -186,82 +319,230 @@ class Haro { return a; }, new Set())).map(i => this.get(i, raw)); } + if (!raw && this.immutable) { + result = Object.freeze(result); + } - return raw ? result : this.list(...result); + return result; } + /** + * Filters records using a predicate function, similar to Array.filter + * @param {Function} fn - Predicate function to test each record (record, key, store) + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records that pass the predicate test + * @throws {Error} Throws error if fn is not a function + * @example + * const adults = store.filter(record => record.age >= 18); + * const recent = store.filter(record => record.created > Date.now() - 86400000); + */ filter (fn, raw = false) { if (typeof fn !== STRING_FUNCTION) { throw new Error(STRING_INVALID_FUNCTION); } - const x = raw ? (k, v) => v : (k, v) => Object.freeze([k, Object.freeze(v)]); - const result = this.reduce((a, v, k, ctx) => { - if (fn.call(ctx, v)) { - a.push(x(k, v)); + let result = this.reduce((a, v) => { + if (fn(v)) { + a.push(v); } return a; }, []); + if (!raw) { + result = result.map(i => this.list(i)); - return raw ? result : Object.freeze(result); + if (this.immutable) { + result = Object.freeze(result); + } + } + + return result; } - forEach (fn, ctx) { - this.data.forEach((value, key) => fn(this.clone(value), this.clone(key)), ctx ?? this.data); + /** + * Executes a function for each record in the store, similar to Array.forEach + * @param {Function} fn - Function to execute for each record (value, key) + * @param {*} [ctx] - Context object to use as 'this' when executing the function + * @returns {Haro} This instance for method chaining + * @example + * store.forEach((record, key) => { + * console.log(`${key}: ${record.name}`); + * }); + */ + forEach (fn, ctx = this) { + this.data.forEach((value, key) => { + if (this.immutable) { + value = this.clone(value); + } + fn.call(ctx, value, key); + }, this); return this; } + /** + * Creates a frozen array from the given arguments for immutable data handling + * @param {...*} args - Arguments to freeze into an array + * @returns {Array<*>} Frozen array containing frozen arguments + * @example + * const frozen = store.freeze(obj1, obj2, obj3); + * // Returns Object.freeze([Object.freeze(obj1), Object.freeze(obj2), Object.freeze(obj3)]) + */ + freeze (...args) { + return Object.freeze(args.map(i => Object.freeze(i))); + } + + /** + * Retrieves a record by its key + * @param {string} key - Key of record to retrieve + * @param {boolean} [raw=false] - Whether to return raw data (true) or processed/frozen data (false) + * @returns {Object|null} The record if found, null if not found + * @example + * const user = store.get('user123'); + * const rawUser = store.get('user123', true); + */ get (key, raw = false) { - const result = this.clone(this.data.get(key) ?? null); + let result = this.data.get(key) ?? null; + if (result !== null && !raw) { + result = this.list(result); + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : this.list(key, result); + return result; } + /** + * Checks if a record with the specified key exists in the store + * @param {string} key - Key to check for existence + * @returns {boolean} True if record exists, false otherwise + * @example + * if (store.has('user123')) { + * console.log('User exists'); + * } + */ has (key) { return this.data.has(key); } + /** + * Generates index keys for composite indexes from data values + * @param {string} [arg=STRING_EMPTY] - Composite index field names joined by delimiter + * @param {string} [delimiter=STRING_PIPE] - Delimiter used in composite index + * @param {Object} [data={}] - Data object to extract field values from + * @returns {string[]} Array of generated index keys + * @example + * // For index 'name|department' with data {name: 'John', department: 'IT'} + * const keys = store.indexKeys('name|department', '|', data); + * // Returns ['John|IT'] + */ indexKeys (arg = STRING_EMPTY, delimiter = STRING_PIPE, data = {}) { - return arg.split(delimiter).reduce((a, li, lidx) => { - const result = []; - - (Array.isArray(data[li]) ? data[li] : [data[li]]).forEach(lli => lidx === INT_0 ? result.push(lli) : a.forEach(x => result.push(`${x}${delimiter}${lli}`))); + const fields = arg.split(delimiter).sort(this.sortKeys); + const fieldsLen = fields.length; + let result = [""]; + for (let i = 0; i < fieldsLen; i++) { + const field = fields[i]; + const values = Array.isArray(data[field]) ? data[field] : [data[field]]; + const newResult = []; + const resultLen = result.length; + const valuesLen = values.length; + for (let j = 0; j < resultLen; j++) { + for (let k = 0; k < valuesLen; k++) { + const newKey = i === 0 ? values[k] : `${result[j]}${delimiter}${values[k]}`; + newResult.push(newKey); + } + } + result = newResult; + } - return result; - }, []); + return result; } + /** + * Returns an iterator of all keys in the store + * @returns {Iterator} Iterator of record keys + * @example + * for (const key of store.keys()) { + * console.log(key); + * } + */ keys () { return this.data.keys(); } + /** + * Returns a limited subset of records with offset support for pagination + * @param {number} [offset=INT_0] - Number of records to skip from the beginning + * @param {number} [max=INT_0] - Maximum number of records to return + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records within the specified range + * @example + * const page1 = store.limit(0, 10); // First 10 records + * const page2 = store.limit(10, 10); // Next 10 records + */ limit (offset = INT_0, max = INT_0, raw = false) { - const result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw)); + let result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw)); + if (!raw && this.immutable) { + result = Object.freeze(result); + } - return raw ? result : this.list(...result); + return result; } - list (...args) { - return Object.freeze(args.map(i => Object.freeze(i))); + /** + * Converts a record into a [key, value] pair array format + * @param {Object} arg - Record object to convert to list format + * @returns {Array<*>} Array containing [key, record] where key is extracted from record's key field + * @example + * const record = {id: 'user123', name: 'John', age: 30}; + * const pair = store.list(record); // ['user123', {id: 'user123', name: 'John', age: 30}] + */ + list (arg) { + const result = [arg[this.key], arg]; + + return this.immutable ? this.freeze(...result) : result; } + /** + * Transforms all records using a mapping function, similar to Array.map + * @param {Function} fn - Function to transform each record (record, key) + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array<*>} Array of transformed results + * @throws {Error} Throws error if fn is not a function + * @example + * const names = store.map(record => record.name); + * const summaries = store.map(record => ({id: record.id, name: record.name})); + */ map (fn, raw = false) { if (typeof fn !== STRING_FUNCTION) { throw new Error(STRING_INVALID_FUNCTION); } - - const result = []; - + let result = []; this.forEach((value, key) => result.push(fn(value, key))); + if (!raw) { + result = result.map(i => this.list(i)); + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : this.list(...result); + return result; } + /** + * Merges two values together with support for arrays and objects + * @param {*} a - First value (target) + * @param {*} b - Second value (source) + * @param {boolean} [override=false] - Whether to override arrays instead of concatenating + * @returns {*} Merged result + * @example + * const merged = store.merge({a: 1}, {b: 2}); // {a: 1, b: 2} + * const arrays = store.merge([1, 2], [3, 4]); // [1, 2, 3, 4] + */ merge (a, b, override = false) { if (Array.isArray(a) && Array.isArray(b)) { a = override ? b : a.concat(b); - } else if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) { + } else if (typeof a === STRING_OBJECT && a !== null && typeof b === STRING_OBJECT && b !== null) { this.each(Object.keys(b), i => { a[i] = this.merge(a[i], b[i], override); }); @@ -272,29 +553,71 @@ class Haro { return a; } + /** + * Lifecycle hook executed after batch operations for custom postprocessing + * @param {Array} arg - Result of batch operation + * @param {string} [type=STRING_EMPTY] - Type of batch operation that was performed + * @returns {Array} Modified result (override this method to implement custom logic) + */ onbatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars return arg; } + /** + * Lifecycle hook executed after clear operation for custom postprocessing + * @returns {void} Override this method in subclasses to implement custom logic + * @example + * class MyStore extends Haro { + * onclear() { + * console.log('Store cleared'); + * } + * } + */ onclear () { // Hook for custom logic after clear; override in subclass if needed } - ondelete (key = STRING_EMPTY, batch = false) { - return [key, batch]; + /** + * Lifecycle hook executed after delete operation for custom postprocessing + * @param {string} [key=STRING_EMPTY] - Key of deleted record + * @param {boolean} [batch=false] - Whether this was part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + ondelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic after delete; override in subclass if needed } - onoverride (type = STRING_EMPTY) { - return type; + /** + * Lifecycle hook executed after override operation for custom postprocessing + * @param {string} [type=STRING_EMPTY] - Type of override operation that was performed + * @returns {void} Override this method in subclasses to implement custom logic + */ + onoverride (type = STRING_EMPTY) { // eslint-disable-line no-unused-vars + // Hook for custom logic after override; override in subclass if needed } - onset (arg = {}, batch = false) { - return [arg, batch]; + /** + * Lifecycle hook executed after set operation for custom postprocessing + * @param {Object} [arg={}] - Record that was set + * @param {boolean} [batch=false] - Whether this was part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + onset (arg = {}, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic after set; override in subclass if needed } + /** + * Replaces all store data or indexes with new data for bulk operations + * @param {Array} data - Data to replace with (format depends on type) + * @param {string} [type=STRING_RECORDS] - Type of data: 'records' or 'indexes' + * @returns {boolean} True if operation succeeded + * @throws {Error} Throws error if type is invalid + * @example + * const records = [['key1', {name: 'John'}], ['key2', {name: 'Jane'}]]; + * store.override(records, 'records'); + */ override (data, type = STRING_RECORDS) { const result = true; - if (type === STRING_INDEXES) { this.indexes = new Map(data.map(i => [i[0], new Map(i[1].map(ii => [ii[0], new Set(ii[1])]))])); } else if (type === STRING_RECORDS) { @@ -303,65 +626,109 @@ class Haro { } else { throw new Error(STRING_INVALID_TYPE); } - this.onoverride(type); return result; } - reduce (fn, accumulator, raw = false) { - let a = accumulator ?? this.data.keys().next().value; - + /** + * Reduces all records to a single value using a reducer function + * @param {Function} fn - Reducer function (accumulator, value, key, store) + * @param {*} [accumulator] - Initial accumulator value + * @returns {*} Final reduced value + * @example + * const totalAge = store.reduce((sum, record) => sum + record.age, 0); + * const names = store.reduce((acc, record) => acc.concat(record.name), []); + */ + reduce (fn, accumulator = []) { + let a = accumulator; this.forEach((v, k) => { - a = fn(a, v, k, this, raw); + a = fn(a, v, k, this); }, this); return a; } + /** + * Rebuilds indexes for specified fields or all fields for data consistency + * @param {string|string[]} [index] - Specific index field(s) to rebuild, or all if not specified + * @returns {Haro} This instance for method chaining + * @example + * store.reindex(); // Rebuild all indexes + * store.reindex('name'); // Rebuild only name index + * store.reindex(['name', 'email']); // Rebuild name and email indexes + */ reindex (index) { const indices = index ? [index] : this.index; - if (index && this.index.includes(index) === false) { this.index.push(index); } - this.each(indices, i => this.indexes.set(i, new Map())); - this.forEach((data, key) => this.each(indices, i => this.setIndex(this.index, this.indexes, this.delimiter, key, data, i))); + this.forEach((data, key) => this.each(indices, i => this.setIndex(key, data, i))); return this; } + /** + * Searches for records containing a value across specified indexes + * @param {*} value - Value to search for (string, function, or RegExp) + * @param {string|string[]} [index] - Index(es) to search in, or all if not specified + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of matching records + * @example + * const results = store.search('john'); // Search all indexes + * const nameResults = store.search('john', 'name'); // Search only name index + * const regexResults = store.search(/^admin/, 'role'); // Regex search + */ search (value, index, raw = false) { - const result = new Map(), - fn = typeof value === STRING_FUNCTION, - rgex = value && typeof value.test === STRING_FUNCTION; - - if (value) { - this.each(index ? Array.isArray(index) ? index : [index] : this.index, i => { - let idx = this.indexes.get(i); - - if (idx) { - idx.forEach((lset, lkey) => { - switch (true) { - case fn && value(lkey, i): - case rgex && value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey): - case lkey === value: - lset.forEach(key => { - if (result.has(key) === false && this.data.has(key)) { - result.set(key, this.get(key, raw)); - } - }); - break; + const result = new Set(); // Use Set for unique keys + const fn = typeof value === STRING_FUNCTION; + const rgex = value && typeof value.test === STRING_FUNCTION; + if (!value) return this.immutable ? this.freeze() : []; + const indices = index ? Array.isArray(index) ? index : [index] : this.index; + for (const i of indices) { + const idx = this.indexes.get(i); + if (idx) { + for (const [lkey, lset] of idx) { + let match = false; + + if (fn) { + match = value(lkey, i); + } else if (rgex) { + match = value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey); + } else { + match = lkey === value; + } + + if (match) { + for (const key of lset) { + if (this.data.has(key)) { + result.add(key); + } } - }); + } } - }); + } + } + let records = Array.from(result).map(key => this.get(key, raw)); + if (!raw && this.immutable) { + records = Object.freeze(records); } - return raw ? Array.from(result.values()) : this.list(...Array.from(result.values())); + return records; } + /** + * Sets or updates a record in the store with automatic indexing + * @param {string|null} [key=null] - Key for the record, or null to use record's key field + * @param {Object} [data={}] - Record data to set + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @param {boolean} [override=false] - Whether to override existing data instead of merging + * @returns {Object} The stored record (frozen if immutable mode) + * @example + * const user = store.set(null, {name: 'John', age: 30}); // Auto-generate key + * const updated = store.set('user123', {age: 31}); // Update existing record + */ set (key = null, data = {}, batch = false, override = false) { if (key === null) { key = data[this.key] ?? this.uuid(); @@ -374,7 +741,7 @@ class Haro { } } else { const og = this.get(key, true); - this.delIndex(this.index, this.indexes, this.delimiter, key, og); + this.deleteIndex(key, og); if (this.versioning) { this.versions.get(key).add(Object.freeze(this.clone(og))); } @@ -383,66 +750,128 @@ class Haro { } } this.data.set(key, x); - this.setIndex(this.index, this.indexes, this.delimiter, key, x, null); + this.setIndex(key, x, null); const result = this.get(key); this.onset(result, batch); return result; } - setIndex (index, indexes, delimiter, key, data, indice) { - this.each(indice === null ? index : [indice], i => { - let lindex = indexes.get(i); - if (!lindex) { - lindex = new Map(); - indexes.set(i, lindex); + /** + * Internal method to add entries to indexes for a record + * @param {string} key - Key of record being indexed + * @param {Object} data - Data of record being indexed + * @param {string|null} indice - Specific index to update, or null for all + * @returns {Haro} This instance for method chaining + */ + setIndex (key, data, indice) { + this.each(indice === null ? this.index : [indice], i => { + let idx = this.indexes.get(i); + if (!idx) { + idx = new Map(); + this.indexes.set(i, idx); } - if (i.includes(delimiter)) { - this.each(this.indexKeys(i, delimiter, data), c => { - if (!lindex.has(c)) { - lindex.set(c, new Set()); - } - lindex.get(c).add(key); - }); + const fn = c => { + if (!idx.has(c)) { + idx.set(c, new Set()); + } + idx.get(c).add(key); + }; + if (i.includes(this.delimiter)) { + this.each(this.indexKeys(i, this.delimiter, data), fn); } else { - this.each(Array.isArray(data[i]) ? data[i] : [data[i]], d => { - if (!lindex.has(d)) { - lindex.set(d, new Set()); - } - lindex.get(d).add(key); - }); + this.each(Array.isArray(data[i]) ? data[i] : [data[i]], fn); } }); + + return this; + } + + /** + * Sorts all records using a comparator function + * @param {Function} fn - Comparator function for sorting (a, b) => number + * @param {boolean} [frozen=false] - Whether to return frozen records + * @returns {Array} Sorted array of records + * @example + * const sorted = store.sort((a, b) => a.age - b.age); // Sort by age + * const names = store.sort((a, b) => a.name.localeCompare(b.name)); // Sort by name + */ + sort (fn, frozen = false) { + const dataSize = this.data.size; + let result = this.limit(INT_0, dataSize, true).sort(fn); + if (frozen) { + result = this.freeze(...result); + } + + return result; } - sort (fn, frozen = true) { - return frozen ? Object.freeze(this.limit(INT_0, this.data.size, true).sort(fn).map(i => Object.freeze(i))) : this.limit(INT_0, this.data.size, true).sort(fn); + /** + * Comparator function for sorting keys with type-aware comparison logic + * @param {*} a - First value to compare + * @param {*} b - Second value to compare + * @returns {number} Negative number if a < b, positive if a > b, zero if equal + * @example + * const keys = ['name', 'age', 'email']; + * keys.sort(store.sortKeys); // Alphabetical sort + * + * const mixed = [10, '5', 'abc', 3]; + * mixed.sort(store.sortKeys); // Type-aware sort: numbers first, then strings + */ + sortKeys (a, b) { + // Handle string comparison + if (typeof a === STRING_STRING && typeof b === STRING_STRING) { + return a.localeCompare(b); + } + // Handle numeric comparison + if (typeof a === STRING_NUMBER && typeof b === STRING_NUMBER) { + return a - b; + } + + // Handle mixed types or other types by converting to string + + return String(a).localeCompare(String(b)); } + /** + * Sorts records by a specific indexed field in ascending order + * @param {string} [index=STRING_EMPTY] - Index field name to sort by + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records sorted by the specified field + * @throws {Error} Throws error if index field is empty or invalid + * @example + * const byAge = store.sortBy('age'); + * const byName = store.sortBy('name'); + */ sortBy (index = STRING_EMPTY, raw = false) { if (index === STRING_EMPTY) { throw new Error(STRING_INVALID_FIELD); } - - const result = [], - keys = []; - + let result = []; + const keys = []; if (this.indexes.has(index) === false) { this.reindex(index); } - const lindex = this.indexes.get(index); - lindex.forEach((idx, key) => keys.push(key)); - this.each(keys.sort(), i => lindex.get(i).forEach(key => result.push(this.get(key, raw)))); + this.each(keys.sort(this.sortKeys), i => lindex.get(i).forEach(key => result.push(this.get(key, raw)))); + if (this.immutable) { + result = Object.freeze(result); + } - return raw ? result : this.list(...result); + return result; } - toArray (frozen = true) { + /** + * Converts all store data to a plain array of records + * @returns {Array} Array containing all records in the store + * @example + * const allRecords = store.toArray(); + * console.log(`Store contains ${allRecords.length} records`); + */ + toArray () { const result = Array.from(this.data.values()); - - if (frozen) { + if (this.immutable) { this.each(result, i => Object.freeze(i)); Object.freeze(result); } @@ -450,61 +879,142 @@ class Haro { return result; } + /** + * Generates a RFC4122 v4 UUID for record identification + * @returns {string} UUID string in standard format + * @example + * const id = store.uuid(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479" + */ uuid () { - return uuid(); + return crypto.randomUUID(); } + /** + * Returns an iterator of all values in the store + * @returns {Iterator} Iterator of record values + * @example + * for (const record of store.values()) { + * console.log(record.name); + * } + */ values () { return this.data.values(); } - where (predicate = {}, raw = false, op = STRING_DOUBLE_PIPE) { - const keys = this.index.filter(i => i in predicate); + /** + * Internal helper method for predicate matching with support for arrays and regex + * @param {Object} record - Record to test against predicate + * @param {Object} predicate - Predicate object with field-value pairs + * @param {string} op - Operator for array matching ('||' for OR, '&&' for AND) + * @returns {boolean} True if record matches predicate criteria + */ + matchesPredicate (record, predicate, op) { + const keys = Object.keys(predicate); + + return keys.every(key => { + const pred = predicate[key]; + const val = record[key]; + if (Array.isArray(pred)) { + if (Array.isArray(val)) { + return op === STRING_DOUBLE_AND ? pred.every(p => val.includes(p)) : pred.some(p => val.includes(p)); + } else { + return op === STRING_DOUBLE_AND ? pred.every(p => val === p) : pred.some(p => val === p); + } + } else if (pred instanceof RegExp) { + if (Array.isArray(val)) { + return op === STRING_DOUBLE_AND ? val.every(v => pred.test(v)) : val.some(v => pred.test(v)); + } else { + return pred.test(val); + } + } else if (Array.isArray(val)) { + return val.includes(pred); + } else { + return val === pred; + } + }); + } + /** + * Advanced filtering with predicate logic supporting AND/OR operations on arrays + * @param {Object} [predicate={}] - Object with field-value pairs for filtering + * @param {string} [op=STRING_DOUBLE_PIPE] - Operator for array matching ('||' for OR, '&&' for AND) + * @returns {Array} Array of records matching the predicate criteria + * @example + * // Find records with tags containing 'admin' OR 'user' + * const users = store.where({tags: ['admin', 'user']}, '||'); + * + * // Find records with ALL specified tags + * const powerUsers = store.where({tags: ['admin', 'power']}, '&&'); + * + * // Regex matching + * const emails = store.where({email: /^admin@/}); + */ + where (predicate = {}, op = STRING_DOUBLE_PIPE) { + const keys = this.index.filter(i => i in predicate); if (keys.length === 0) return []; - // Supported operators: '||' (OR), '&&' (AND) - // Always AND across fields (all keys must match for a record) - return this.filter(a => { - const matches = keys.map(i => { - const pred = predicate[i]; - const val = a[i]; + // Try to use indexes for better performance + const indexedKeys = keys.filter(k => this.indexes.has(k)); + if (indexedKeys.length > 0) { + // Use index-based filtering for better performance + let candidateKeys = new Set(); + let first = true; + for (const key of indexedKeys) { + const pred = predicate[key]; + const idx = this.indexes.get(key); + const matchingKeys = new Set(); if (Array.isArray(pred)) { - if (Array.isArray(val)) { - if (op === "&&") { - return pred.every(p => val.includes(p)); - } else { - return pred.some(p => val.includes(p)); + for (const p of pred) { + if (idx.has(p)) { + for (const k of idx.get(p)) { + matchingKeys.add(k); + } } - } else if (op === "&&") { - return pred.every(p => val === p); - } else { - return pred.some(p => val === p); } - } else if (pred instanceof RegExp) { - if (Array.isArray(val)) { - if (op === "&&") { - return val.every(v => pred.test(v)); - } else { - return val.some(v => pred.test(v)); - } - } else { - return pred.test(val); + } else if (idx.has(pred)) { + for (const k of idx.get(pred)) { + matchingKeys.add(k); } - } else if (Array.isArray(val)) { - return val.includes(pred); + } + if (first) { + candidateKeys = matchingKeys; + first = false; } else { - return val === pred; + // AND operation across different fields + candidateKeys = new Set([...candidateKeys].filter(k => matchingKeys.has(k))); } - }); - const isMatch = matches.every(Boolean); + } + // Filter candidates with full predicate logic + const results = []; + for (const key of candidateKeys) { + const record = this.get(key, true); + if (this.matchesPredicate(record, predicate, op)) { + results.push(this.immutable ? this.get(key) : record); + } + } - return isMatch; - }, raw); - } + return this.immutable ? this.freeze(...results) : results; + } + // Fallback to full scan if no indexes available + return this.filter(a => this.matchesPredicate(a, predicate, op)); + } } +/** + * Factory function to create a new Haro instance with optional initial data + * @param {Array|null} [data=null] - Initial data to populate the store + * @param {Object} [config={}] - Configuration object passed to Haro constructor + * @returns {Haro} New Haro instance configured and optionally populated + * @example + * const store = haro([ + * {id: 1, name: 'John', age: 30}, + * {id: 2, name: 'Jane', age: 25} + * ], { + * index: ['name', 'age'], + * versioning: true + * }); + */ function haro (data = null, config = {}) { const obj = new Haro(config); diff --git a/dist/haro.js b/dist/haro.js index ad2dde73..26aed958 100644 --- a/dist/haro.js +++ b/dist/haro.js @@ -3,56 +3,79 @@ * * @copyright 2025 Jason Mulligan * @license BSD-3-Clause - * @version 15.2.6 + * @version 16.0.0 */ +import {randomUUID}from'crypto';// String constants - Single characters and symbols const STRING_COMMA = ","; const STRING_EMPTY = ""; const STRING_PIPE = "|"; const STRING_DOUBLE_PIPE = "||"; -const STRING_A = "a"; -const STRING_B = "b"; +const STRING_DOUBLE_AND = "&&"; + +// String constants - Operation and type names +const STRING_ID = "id"; const STRING_DEL = "del"; const STRING_FUNCTION = "function"; const STRING_INDEXES = "indexes"; -const STRING_INVALID_FIELD = "Invalid field"; -const STRING_INVALID_FUNCTION = "Invalid function"; -const STRING_INVALID_TYPE = "Invalid type"; const STRING_OBJECT = "object"; -const STRING_RECORD_NOT_FOUND = "Record not found"; const STRING_RECORDS = "records"; const STRING_REGISTRY = "registry"; const STRING_SET = "set"; const STRING_SIZE = "size"; -const INT_0 = 0; -const INT_1 = 1; -const INT_3 = 3; -const INT_4 = 4; -const INT_8 = 8; -const INT_9 = 9; -const INT_16 = 16;/* istanbul ignore next */ -const r = [INT_8, INT_9, STRING_A, STRING_B]; - -/* istanbul ignore next */ -function s () { - return ((Math.random() + INT_1) * 0x10000 | INT_0).toString(INT_16).substring(INT_1); -} +const STRING_STRING = "string"; +const STRING_NUMBER = "number"; -/* istanbul ignore next */ -function randomUUID () { - return `${s()}${s()}-${s()}-4${s().slice(INT_0, INT_3)}-${r[Math.floor(Math.random() * INT_4)]}${s().slice(INT_0, INT_3)}-${s()}${s()}${s()}`; -} +// String constants - Error messages +const STRING_INVALID_FIELD = "Invalid field"; +const STRING_INVALID_FUNCTION = "Invalid function"; +const STRING_INVALID_TYPE = "Invalid type"; +const STRING_RECORD_NOT_FOUND = "Record not found"; -const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : randomUUID;class Haro { - constructor ({delimiter = STRING_PIPE, id = this.uuid(), index = [], key = "id", versioning = false} = {}) { +// Integer constants +const INT_0 = 0;/** + * Haro is a modern immutable DataStore for collections of records with indexing, + * versioning, and batch operations support. It provides a Map-like interface + * with advanced querying capabilities through indexes. + * @class + * @example + * const store = new Haro({ + * index: ['name', 'age'], + * key: 'id', + * versioning: true + * }); + * + * store.set(null, {name: 'John', age: 30}); + * const results = store.find({name: 'John'}); + */ +class Haro { + /** + * Creates a new Haro instance with specified configuration + * @param {Object} [config={}] - Configuration object for the store + * @param {string} [config.delimiter=STRING_PIPE] - Delimiter for composite indexes (default: '|') + * @param {string} [config.id] - Unique identifier for this instance (auto-generated if not provided) + * @param {boolean} [config.immutable=false] - Return frozen/immutable objects for data safety + * @param {string[]} [config.index=[]] - Array of field names to create indexes for + * @param {string} [config.key=STRING_ID] - Primary key field name used for record identification + * @param {boolean} [config.versioning=false] - Enable versioning to track record changes + * @constructor + * @example + * const store = new Haro({ + * index: ['name', 'email', 'name|department'], + * key: 'userId', + * versioning: true, + * immutable: true + * }); + */ + constructor ({delimiter = STRING_PIPE, id = this.uuid(), immutable = false, index = [], key = STRING_ID, versioning = false} = {}) { this.data = new Map(); this.delimiter = delimiter; this.id = id; + this.immutable = immutable; this.index = Array.isArray(index) ? [...index] : []; this.indexes = new Map(); this.key = key; this.versions = new Map(); this.versioning = versioning; - Object.defineProperty(this, STRING_REGISTRY, { enumerable: true, get: () => Array.from(this.data.keys()) @@ -65,28 +88,78 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return this.reindex(); } + /** + * Performs batch operations on multiple records for efficient bulk processing + * @param {Array} args - Array of records to process + * @param {string} [type=STRING_SET] - Type of operation: 'set' for upsert, 'del' for delete + * @returns {Array} Array of results from the batch operation + * @throws {Error} Throws error if individual operations fail during batch processing + * @example + * const results = store.batch([ + * {id: 1, name: 'John'}, + * {id: 2, name: 'Jane'} + * ], 'set'); + */ batch (args, type = STRING_SET) { - const fn = type === STRING_DEL ? i => this.del(i, true) : i => this.set(null, i, true, true); + const fn = type === STRING_DEL ? i => this.delete(i, true) : i => this.set(null, i, true, true); return this.onbatch(this.beforeBatch(args, type).map(fn), type); } + /** + * Lifecycle hook executed before batch operations for custom preprocessing + * @param {Array} arg - Arguments passed to batch operation + * @param {string} [type=STRING_EMPTY] - Type of batch operation ('set' or 'del') + * @returns {Array} The arguments array (possibly modified) to be processed + */ beforeBatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars + // Hook for custom logic before batch; override in subclass if needed return arg; } + /** + * Lifecycle hook executed before clear operation for custom preprocessing + * @returns {void} Override this method in subclasses to implement custom logic + * @example + * class MyStore extends Haro { + * beforeClear() { + * this.backup = this.toArray(); + * } + * } + */ beforeClear () { // Hook for custom logic before clear; override in subclass if needed } - beforeDelete (key = STRING_EMPTY, batch = false) { - return [key, batch]; - } - - beforeSet (key = STRING_EMPTY, batch = false) { - return [key, batch]; - } - + /** + * Lifecycle hook executed before delete operation for custom preprocessing + * @param {string} [key=STRING_EMPTY] - Key of record to delete + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + beforeDelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic before delete; override in subclass if needed + } + + /** + * Lifecycle hook executed before set operation for custom preprocessing + * @param {string} [key=STRING_EMPTY] - Key of record to set + * @param {Object} [data={}] - Record data being set + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @param {boolean} [override=false] - Whether to override existing data + * @returns {void} Override this method in subclasses to implement custom logic + */ + beforeSet (key = STRING_EMPTY, data = {}, batch = false, override = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic before set; override in subclass if needed + } + + /** + * Removes all records, indexes, and versions from the store + * @returns {Haro} This instance for method chaining + * @example + * store.clear(); + * console.log(store.size); // 0 + */ clear () { this.beforeClear(); this.data.clear(); @@ -97,17 +170,36 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return this; } + /** + * Creates a deep clone of the given value, handling objects, arrays, and primitives + * @param {*} arg - Value to clone (any type) + * @returns {*} Deep clone of the argument + * @example + * const original = {name: 'John', tags: ['user', 'admin']}; + * const cloned = store.clone(original); + * cloned.tags.push('new'); // original.tags is unchanged + */ clone (arg) { - return JSON.parse(JSON.stringify(arg)); - } - - del (key = STRING_EMPTY, batch = false) { + return structuredClone(arg); + } + + /** + * Deletes a record from the store and removes it from all indexes + * @param {string} [key=STRING_EMPTY] - Key of record to delete + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @returns {void} + * @throws {Error} Throws error if record with the specified key is not found + * @example + * store.delete('user123'); + * // Throws error if 'user123' doesn't exist + */ + delete (key = STRING_EMPTY, batch = false) { if (!this.data.has(key)) { throw new Error(STRING_RECORD_NOT_FOUND); } const og = this.get(key, true); this.beforeDelete(key, batch); - this.delIndex(this.index, this.indexes, this.delimiter, key, og); + this.deleteIndex(key, og); this.data.delete(key); this.ondelete(key, batch); if (this.versioning) { @@ -115,12 +207,18 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } } - delIndex (index, indexes, delimiter, key, data) { - index.forEach(i => { - const idx = indexes.get(i); + /** + * Internal method to remove entries from indexes for a deleted record + * @param {string} key - Key of record being deleted + * @param {Object} data - Data of record being deleted + * @returns {Haro} This instance for method chaining + */ + deleteIndex (key, data) { + this.index.forEach(i => { + const idx = this.indexes.get(i); if (!idx) return; - const values = i.includes(delimiter) ? - this.indexKeys(i, delimiter, data) : + const values = i.includes(this.delimiter) ? + this.indexKeys(i, this.delimiter, data) : Array.isArray(data[i]) ? data[i] : [data[i]]; this.each(values, value => { if (idx.has(value)) { @@ -132,11 +230,20 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } }); }); + + return this; } + /** + * Exports complete store data or indexes for persistence or debugging + * @param {string} [type=STRING_RECORDS] - Type of data to export: 'records' or 'indexes' + * @returns {Array} Array of [key, value] pairs for records, or serialized index structure + * @example + * const records = store.dump('records'); + * const indexes = store.dump('indexes'); + */ dump (type = STRING_RECORDS) { let result; - if (type === STRING_RECORDS) { result = Array.from(this.entries()); } else { @@ -154,20 +261,46 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return result; } + /** + * Utility method to iterate over an array with a callback function + * @param {Array<*>} [arr=[]] - Array to iterate over + * @param {Function} fn - Function to call for each element (element, index) + * @returns {Array<*>} The original array for method chaining + * @example + * store.each([1, 2, 3], (item, index) => console.log(item, index)); + */ each (arr = [], fn) { - for (const [idx, value] of arr.entries()) { - fn(value, idx); + const len = arr.length; + for (let i = 0; i < len; i++) { + fn(arr[i], i); } return arr; } + /** + * Returns an iterator of [key, value] pairs for each record in the store + * @returns {Iterator>} Iterator of [key, value] pairs + * @example + * for (const [key, value] of store.entries()) { + * console.log(key, value); + * } + */ entries () { return this.data.entries(); } + /** + * Finds records matching the specified criteria using indexes for optimal performance + * @param {Object} [where={}] - Object with field-value pairs to match against + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of matching records (frozen if immutable mode) + * @example + * const users = store.find({department: 'engineering', active: true}); + * const admins = store.find({role: 'admin'}); + */ find (where = {}, raw = false) { - const key = Object.keys(where).sort((a, b) => a.localeCompare(b)).join(this.delimiter); + const key = Object.keys(where).sort(this.sortKeys).join(this.delimiter); const index = this.indexes.get(key) ?? new Map(); let result = []; if (index.size > 0) { @@ -180,82 +313,230 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return a; }, new Set())).map(i => this.get(i, raw)); } + if (!raw && this.immutable) { + result = Object.freeze(result); + } - return raw ? result : this.list(...result); + return result; } + /** + * Filters records using a predicate function, similar to Array.filter + * @param {Function} fn - Predicate function to test each record (record, key, store) + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records that pass the predicate test + * @throws {Error} Throws error if fn is not a function + * @example + * const adults = store.filter(record => record.age >= 18); + * const recent = store.filter(record => record.created > Date.now() - 86400000); + */ filter (fn, raw = false) { if (typeof fn !== STRING_FUNCTION) { throw new Error(STRING_INVALID_FUNCTION); } - const x = raw ? (k, v) => v : (k, v) => Object.freeze([k, Object.freeze(v)]); - const result = this.reduce((a, v, k, ctx) => { - if (fn.call(ctx, v)) { - a.push(x(k, v)); + let result = this.reduce((a, v) => { + if (fn(v)) { + a.push(v); } return a; }, []); + if (!raw) { + result = result.map(i => this.list(i)); + + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : Object.freeze(result); + return result; } - forEach (fn, ctx) { - this.data.forEach((value, key) => fn(this.clone(value), this.clone(key)), ctx ?? this.data); + /** + * Executes a function for each record in the store, similar to Array.forEach + * @param {Function} fn - Function to execute for each record (value, key) + * @param {*} [ctx] - Context object to use as 'this' when executing the function + * @returns {Haro} This instance for method chaining + * @example + * store.forEach((record, key) => { + * console.log(`${key}: ${record.name}`); + * }); + */ + forEach (fn, ctx = this) { + this.data.forEach((value, key) => { + if (this.immutable) { + value = this.clone(value); + } + fn.call(ctx, value, key); + }, this); return this; } + /** + * Creates a frozen array from the given arguments for immutable data handling + * @param {...*} args - Arguments to freeze into an array + * @returns {Array<*>} Frozen array containing frozen arguments + * @example + * const frozen = store.freeze(obj1, obj2, obj3); + * // Returns Object.freeze([Object.freeze(obj1), Object.freeze(obj2), Object.freeze(obj3)]) + */ + freeze (...args) { + return Object.freeze(args.map(i => Object.freeze(i))); + } + + /** + * Retrieves a record by its key + * @param {string} key - Key of record to retrieve + * @param {boolean} [raw=false] - Whether to return raw data (true) or processed/frozen data (false) + * @returns {Object|null} The record if found, null if not found + * @example + * const user = store.get('user123'); + * const rawUser = store.get('user123', true); + */ get (key, raw = false) { - const result = this.clone(this.data.get(key) ?? null); + let result = this.data.get(key) ?? null; + if (result !== null && !raw) { + result = this.list(result); + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : this.list(key, result); + return result; } + /** + * Checks if a record with the specified key exists in the store + * @param {string} key - Key to check for existence + * @returns {boolean} True if record exists, false otherwise + * @example + * if (store.has('user123')) { + * console.log('User exists'); + * } + */ has (key) { return this.data.has(key); } + /** + * Generates index keys for composite indexes from data values + * @param {string} [arg=STRING_EMPTY] - Composite index field names joined by delimiter + * @param {string} [delimiter=STRING_PIPE] - Delimiter used in composite index + * @param {Object} [data={}] - Data object to extract field values from + * @returns {string[]} Array of generated index keys + * @example + * // For index 'name|department' with data {name: 'John', department: 'IT'} + * const keys = store.indexKeys('name|department', '|', data); + * // Returns ['John|IT'] + */ indexKeys (arg = STRING_EMPTY, delimiter = STRING_PIPE, data = {}) { - return arg.split(delimiter).reduce((a, li, lidx) => { - const result = []; - - (Array.isArray(data[li]) ? data[li] : [data[li]]).forEach(lli => lidx === INT_0 ? result.push(lli) : a.forEach(x => result.push(`${x}${delimiter}${lli}`))); + const fields = arg.split(delimiter).sort(this.sortKeys); + const fieldsLen = fields.length; + let result = [""]; + for (let i = 0; i < fieldsLen; i++) { + const field = fields[i]; + const values = Array.isArray(data[field]) ? data[field] : [data[field]]; + const newResult = []; + const resultLen = result.length; + const valuesLen = values.length; + for (let j = 0; j < resultLen; j++) { + for (let k = 0; k < valuesLen; k++) { + const newKey = i === 0 ? values[k] : `${result[j]}${delimiter}${values[k]}`; + newResult.push(newKey); + } + } + result = newResult; + } - return result; - }, []); + return result; } + /** + * Returns an iterator of all keys in the store + * @returns {Iterator} Iterator of record keys + * @example + * for (const key of store.keys()) { + * console.log(key); + * } + */ keys () { return this.data.keys(); } + /** + * Returns a limited subset of records with offset support for pagination + * @param {number} [offset=INT_0] - Number of records to skip from the beginning + * @param {number} [max=INT_0] - Maximum number of records to return + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records within the specified range + * @example + * const page1 = store.limit(0, 10); // First 10 records + * const page2 = store.limit(10, 10); // Next 10 records + */ limit (offset = INT_0, max = INT_0, raw = false) { - const result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw)); - - return raw ? result : this.list(...result); - } + let result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw)); + if (!raw && this.immutable) { + result = Object.freeze(result); + } - list (...args) { - return Object.freeze(args.map(i => Object.freeze(i))); + return result; } + /** + * Converts a record into a [key, value] pair array format + * @param {Object} arg - Record object to convert to list format + * @returns {Array<*>} Array containing [key, record] where key is extracted from record's key field + * @example + * const record = {id: 'user123', name: 'John', age: 30}; + * const pair = store.list(record); // ['user123', {id: 'user123', name: 'John', age: 30}] + */ + list (arg) { + const result = [arg[this.key], arg]; + + return this.immutable ? this.freeze(...result) : result; + } + + /** + * Transforms all records using a mapping function, similar to Array.map + * @param {Function} fn - Function to transform each record (record, key) + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array<*>} Array of transformed results + * @throws {Error} Throws error if fn is not a function + * @example + * const names = store.map(record => record.name); + * const summaries = store.map(record => ({id: record.id, name: record.name})); + */ map (fn, raw = false) { if (typeof fn !== STRING_FUNCTION) { throw new Error(STRING_INVALID_FUNCTION); } - - const result = []; - + let result = []; this.forEach((value, key) => result.push(fn(value, key))); + if (!raw) { + result = result.map(i => this.list(i)); + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : this.list(...result); + return result; } + /** + * Merges two values together with support for arrays and objects + * @param {*} a - First value (target) + * @param {*} b - Second value (source) + * @param {boolean} [override=false] - Whether to override arrays instead of concatenating + * @returns {*} Merged result + * @example + * const merged = store.merge({a: 1}, {b: 2}); // {a: 1, b: 2} + * const arrays = store.merge([1, 2], [3, 4]); // [1, 2, 3, 4] + */ merge (a, b, override = false) { if (Array.isArray(a) && Array.isArray(b)) { a = override ? b : a.concat(b); - } else if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) { + } else if (typeof a === STRING_OBJECT && a !== null && typeof b === STRING_OBJECT && b !== null) { this.each(Object.keys(b), i => { a[i] = this.merge(a[i], b[i], override); }); @@ -266,29 +547,71 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return a; } + /** + * Lifecycle hook executed after batch operations for custom postprocessing + * @param {Array} arg - Result of batch operation + * @param {string} [type=STRING_EMPTY] - Type of batch operation that was performed + * @returns {Array} Modified result (override this method to implement custom logic) + */ onbatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars return arg; } + /** + * Lifecycle hook executed after clear operation for custom postprocessing + * @returns {void} Override this method in subclasses to implement custom logic + * @example + * class MyStore extends Haro { + * onclear() { + * console.log('Store cleared'); + * } + * } + */ onclear () { // Hook for custom logic after clear; override in subclass if needed } - ondelete (key = STRING_EMPTY, batch = false) { - return [key, batch]; - } - - onoverride (type = STRING_EMPTY) { - return type; - } - - onset (arg = {}, batch = false) { - return [arg, batch]; - } - + /** + * Lifecycle hook executed after delete operation for custom postprocessing + * @param {string} [key=STRING_EMPTY] - Key of deleted record + * @param {boolean} [batch=false] - Whether this was part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + ondelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic after delete; override in subclass if needed + } + + /** + * Lifecycle hook executed after override operation for custom postprocessing + * @param {string} [type=STRING_EMPTY] - Type of override operation that was performed + * @returns {void} Override this method in subclasses to implement custom logic + */ + onoverride (type = STRING_EMPTY) { // eslint-disable-line no-unused-vars + // Hook for custom logic after override; override in subclass if needed + } + + /** + * Lifecycle hook executed after set operation for custom postprocessing + * @param {Object} [arg={}] - Record that was set + * @param {boolean} [batch=false] - Whether this was part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + onset (arg = {}, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic after set; override in subclass if needed + } + + /** + * Replaces all store data or indexes with new data for bulk operations + * @param {Array} data - Data to replace with (format depends on type) + * @param {string} [type=STRING_RECORDS] - Type of data: 'records' or 'indexes' + * @returns {boolean} True if operation succeeded + * @throws {Error} Throws error if type is invalid + * @example + * const records = [['key1', {name: 'John'}], ['key2', {name: 'Jane'}]]; + * store.override(records, 'records'); + */ override (data, type = STRING_RECORDS) { const result = true; - if (type === STRING_INDEXES) { this.indexes = new Map(data.map(i => [i[0], new Map(i[1].map(ii => [ii[0], new Set(ii[1])]))])); } else if (type === STRING_RECORDS) { @@ -297,65 +620,109 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } else { throw new Error(STRING_INVALID_TYPE); } - this.onoverride(type); return result; } - reduce (fn, accumulator, raw = false) { - let a = accumulator ?? this.data.keys().next().value; - + /** + * Reduces all records to a single value using a reducer function + * @param {Function} fn - Reducer function (accumulator, value, key, store) + * @param {*} [accumulator] - Initial accumulator value + * @returns {*} Final reduced value + * @example + * const totalAge = store.reduce((sum, record) => sum + record.age, 0); + * const names = store.reduce((acc, record) => acc.concat(record.name), []); + */ + reduce (fn, accumulator = []) { + let a = accumulator; this.forEach((v, k) => { - a = fn(a, v, k, this, raw); + a = fn(a, v, k, this); }, this); return a; } + /** + * Rebuilds indexes for specified fields or all fields for data consistency + * @param {string|string[]} [index] - Specific index field(s) to rebuild, or all if not specified + * @returns {Haro} This instance for method chaining + * @example + * store.reindex(); // Rebuild all indexes + * store.reindex('name'); // Rebuild only name index + * store.reindex(['name', 'email']); // Rebuild name and email indexes + */ reindex (index) { const indices = index ? [index] : this.index; - if (index && this.index.includes(index) === false) { this.index.push(index); } - this.each(indices, i => this.indexes.set(i, new Map())); - this.forEach((data, key) => this.each(indices, i => this.setIndex(this.index, this.indexes, this.delimiter, key, data, i))); + this.forEach((data, key) => this.each(indices, i => this.setIndex(key, data, i))); return this; } + /** + * Searches for records containing a value across specified indexes + * @param {*} value - Value to search for (string, function, or RegExp) + * @param {string|string[]} [index] - Index(es) to search in, or all if not specified + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of matching records + * @example + * const results = store.search('john'); // Search all indexes + * const nameResults = store.search('john', 'name'); // Search only name index + * const regexResults = store.search(/^admin/, 'role'); // Regex search + */ search (value, index, raw = false) { - const result = new Map(), - fn = typeof value === STRING_FUNCTION, - rgex = value && typeof value.test === STRING_FUNCTION; - - if (value) { - this.each(index ? Array.isArray(index) ? index : [index] : this.index, i => { - let idx = this.indexes.get(i); - - if (idx) { - idx.forEach((lset, lkey) => { - switch (true) { - case fn && value(lkey, i): - case rgex && value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey): - case lkey === value: - lset.forEach(key => { - if (result.has(key) === false && this.data.has(key)) { - result.set(key, this.get(key, raw)); - } - }); - break; + const result = new Set(); // Use Set for unique keys + const fn = typeof value === STRING_FUNCTION; + const rgex = value && typeof value.test === STRING_FUNCTION; + if (!value) return this.immutable ? this.freeze() : []; + const indices = index ? Array.isArray(index) ? index : [index] : this.index; + for (const i of indices) { + const idx = this.indexes.get(i); + if (idx) { + for (const [lkey, lset] of idx) { + let match = false; + + if (fn) { + match = value(lkey, i); + } else if (rgex) { + match = value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey); + } else { + match = lkey === value; + } + + if (match) { + for (const key of lset) { + if (this.data.has(key)) { + result.add(key); + } } - }); + } } - }); + } + } + let records = Array.from(result).map(key => this.get(key, raw)); + if (!raw && this.immutable) { + records = Object.freeze(records); } - return raw ? Array.from(result.values()) : this.list(...Array.from(result.values())); + return records; } + /** + * Sets or updates a record in the store with automatic indexing + * @param {string|null} [key=null] - Key for the record, or null to use record's key field + * @param {Object} [data={}] - Record data to set + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @param {boolean} [override=false] - Whether to override existing data instead of merging + * @returns {Object} The stored record (frozen if immutable mode) + * @example + * const user = store.set(null, {name: 'John', age: 30}); // Auto-generate key + * const updated = store.set('user123', {age: 31}); // Update existing record + */ set (key = null, data = {}, batch = false, override = false) { if (key === null) { key = data[this.key] ?? this.uuid(); @@ -368,7 +735,7 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } } else { const og = this.get(key, true); - this.delIndex(this.index, this.indexes, this.delimiter, key, og); + this.deleteIndex(key, og); if (this.versioning) { this.versions.get(key).add(Object.freeze(this.clone(og))); } @@ -377,66 +744,128 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } } this.data.set(key, x); - this.setIndex(this.index, this.indexes, this.delimiter, key, x, null); + this.setIndex(key, x, null); const result = this.get(key); this.onset(result, batch); return result; } - setIndex (index, indexes, delimiter, key, data, indice) { - this.each(indice === null ? index : [indice], i => { - let lindex = indexes.get(i); - if (!lindex) { - lindex = new Map(); - indexes.set(i, lindex); + /** + * Internal method to add entries to indexes for a record + * @param {string} key - Key of record being indexed + * @param {Object} data - Data of record being indexed + * @param {string|null} indice - Specific index to update, or null for all + * @returns {Haro} This instance for method chaining + */ + setIndex (key, data, indice) { + this.each(indice === null ? this.index : [indice], i => { + let idx = this.indexes.get(i); + if (!idx) { + idx = new Map(); + this.indexes.set(i, idx); } - if (i.includes(delimiter)) { - this.each(this.indexKeys(i, delimiter, data), c => { - if (!lindex.has(c)) { - lindex.set(c, new Set()); - } - lindex.get(c).add(key); - }); + const fn = c => { + if (!idx.has(c)) { + idx.set(c, new Set()); + } + idx.get(c).add(key); + }; + if (i.includes(this.delimiter)) { + this.each(this.indexKeys(i, this.delimiter, data), fn); } else { - this.each(Array.isArray(data[i]) ? data[i] : [data[i]], d => { - if (!lindex.has(d)) { - lindex.set(d, new Set()); - } - lindex.get(d).add(key); - }); + this.each(Array.isArray(data[i]) ? data[i] : [data[i]], fn); } }); + + return this; } - sort (fn, frozen = true) { - return frozen ? Object.freeze(this.limit(INT_0, this.data.size, true).sort(fn).map(i => Object.freeze(i))) : this.limit(INT_0, this.data.size, true).sort(fn); + /** + * Sorts all records using a comparator function + * @param {Function} fn - Comparator function for sorting (a, b) => number + * @param {boolean} [frozen=false] - Whether to return frozen records + * @returns {Array} Sorted array of records + * @example + * const sorted = store.sort((a, b) => a.age - b.age); // Sort by age + * const names = store.sort((a, b) => a.name.localeCompare(b.name)); // Sort by name + */ + sort (fn, frozen = false) { + const dataSize = this.data.size; + let result = this.limit(INT_0, dataSize, true).sort(fn); + if (frozen) { + result = this.freeze(...result); + } + + return result; } + /** + * Comparator function for sorting keys with type-aware comparison logic + * @param {*} a - First value to compare + * @param {*} b - Second value to compare + * @returns {number} Negative number if a < b, positive if a > b, zero if equal + * @example + * const keys = ['name', 'age', 'email']; + * keys.sort(store.sortKeys); // Alphabetical sort + * + * const mixed = [10, '5', 'abc', 3]; + * mixed.sort(store.sortKeys); // Type-aware sort: numbers first, then strings + */ + sortKeys (a, b) { + // Handle string comparison + if (typeof a === STRING_STRING && typeof b === STRING_STRING) { + return a.localeCompare(b); + } + // Handle numeric comparison + if (typeof a === STRING_NUMBER && typeof b === STRING_NUMBER) { + return a - b; + } + + // Handle mixed types or other types by converting to string + + return String(a).localeCompare(String(b)); + } + + /** + * Sorts records by a specific indexed field in ascending order + * @param {string} [index=STRING_EMPTY] - Index field name to sort by + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records sorted by the specified field + * @throws {Error} Throws error if index field is empty or invalid + * @example + * const byAge = store.sortBy('age'); + * const byName = store.sortBy('name'); + */ sortBy (index = STRING_EMPTY, raw = false) { if (index === STRING_EMPTY) { throw new Error(STRING_INVALID_FIELD); } - - const result = [], - keys = []; - + let result = []; + const keys = []; if (this.indexes.has(index) === false) { this.reindex(index); } - const lindex = this.indexes.get(index); - lindex.forEach((idx, key) => keys.push(key)); - this.each(keys.sort(), i => lindex.get(i).forEach(key => result.push(this.get(key, raw)))); + this.each(keys.sort(this.sortKeys), i => lindex.get(i).forEach(key => result.push(this.get(key, raw)))); + if (this.immutable) { + result = Object.freeze(result); + } - return raw ? result : this.list(...result); + return result; } - toArray (frozen = true) { + /** + * Converts all store data to a plain array of records + * @returns {Array} Array containing all records in the store + * @example + * const allRecords = store.toArray(); + * console.log(`Store contains ${allRecords.length} records`); + */ + toArray () { const result = Array.from(this.data.values()); - - if (frozen) { + if (this.immutable) { this.each(result, i => Object.freeze(i)); Object.freeze(result); } @@ -444,61 +873,142 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return result; } + /** + * Generates a RFC4122 v4 UUID for record identification + * @returns {string} UUID string in standard format + * @example + * const id = store.uuid(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479" + */ uuid () { - return uuid(); + return randomUUID(); } + /** + * Returns an iterator of all values in the store + * @returns {Iterator} Iterator of record values + * @example + * for (const record of store.values()) { + * console.log(record.name); + * } + */ values () { return this.data.values(); } - where (predicate = {}, raw = false, op = STRING_DOUBLE_PIPE) { - const keys = this.index.filter(i => i in predicate); + /** + * Internal helper method for predicate matching with support for arrays and regex + * @param {Object} record - Record to test against predicate + * @param {Object} predicate - Predicate object with field-value pairs + * @param {string} op - Operator for array matching ('||' for OR, '&&' for AND) + * @returns {boolean} True if record matches predicate criteria + */ + matchesPredicate (record, predicate, op) { + const keys = Object.keys(predicate); + + return keys.every(key => { + const pred = predicate[key]; + const val = record[key]; + if (Array.isArray(pred)) { + if (Array.isArray(val)) { + return op === STRING_DOUBLE_AND ? pred.every(p => val.includes(p)) : pred.some(p => val.includes(p)); + } else { + return op === STRING_DOUBLE_AND ? pred.every(p => val === p) : pred.some(p => val === p); + } + } else if (pred instanceof RegExp) { + if (Array.isArray(val)) { + return op === STRING_DOUBLE_AND ? val.every(v => pred.test(v)) : val.some(v => pred.test(v)); + } else { + return pred.test(val); + } + } else if (Array.isArray(val)) { + return val.includes(pred); + } else { + return val === pred; + } + }); + } + /** + * Advanced filtering with predicate logic supporting AND/OR operations on arrays + * @param {Object} [predicate={}] - Object with field-value pairs for filtering + * @param {string} [op=STRING_DOUBLE_PIPE] - Operator for array matching ('||' for OR, '&&' for AND) + * @returns {Array} Array of records matching the predicate criteria + * @example + * // Find records with tags containing 'admin' OR 'user' + * const users = store.where({tags: ['admin', 'user']}, '||'); + * + * // Find records with ALL specified tags + * const powerUsers = store.where({tags: ['admin', 'power']}, '&&'); + * + * // Regex matching + * const emails = store.where({email: /^admin@/}); + */ + where (predicate = {}, op = STRING_DOUBLE_PIPE) { + const keys = this.index.filter(i => i in predicate); if (keys.length === 0) return []; - // Supported operators: '||' (OR), '&&' (AND) - // Always AND across fields (all keys must match for a record) - return this.filter(a => { - const matches = keys.map(i => { - const pred = predicate[i]; - const val = a[i]; + // Try to use indexes for better performance + const indexedKeys = keys.filter(k => this.indexes.has(k)); + if (indexedKeys.length > 0) { + // Use index-based filtering for better performance + let candidateKeys = new Set(); + let first = true; + for (const key of indexedKeys) { + const pred = predicate[key]; + const idx = this.indexes.get(key); + const matchingKeys = new Set(); if (Array.isArray(pred)) { - if (Array.isArray(val)) { - if (op === "&&") { - return pred.every(p => val.includes(p)); - } else { - return pred.some(p => val.includes(p)); + for (const p of pred) { + if (idx.has(p)) { + for (const k of idx.get(p)) { + matchingKeys.add(k); + } } - } else if (op === "&&") { - return pred.every(p => val === p); - } else { - return pred.some(p => val === p); } - } else if (pred instanceof RegExp) { - if (Array.isArray(val)) { - if (op === "&&") { - return val.every(v => pred.test(v)); - } else { - return val.some(v => pred.test(v)); - } - } else { - return pred.test(val); + } else if (idx.has(pred)) { + for (const k of idx.get(pred)) { + matchingKeys.add(k); } - } else if (Array.isArray(val)) { - return val.includes(pred); + } + if (first) { + candidateKeys = matchingKeys; + first = false; } else { - return val === pred; + // AND operation across different fields + candidateKeys = new Set([...candidateKeys].filter(k => matchingKeys.has(k))); } - }); - const isMatch = matches.every(Boolean); + } + // Filter candidates with full predicate logic + const results = []; + for (const key of candidateKeys) { + const record = this.get(key, true); + if (this.matchesPredicate(record, predicate, op)) { + results.push(this.immutable ? this.get(key) : record); + } + } - return isMatch; - }, raw); - } + return this.immutable ? this.freeze(...results) : results; + } + // Fallback to full scan if no indexes available + return this.filter(a => this.matchesPredicate(a, predicate, op)); + } } +/** + * Factory function to create a new Haro instance with optional initial data + * @param {Array|null} [data=null] - Initial data to populate the store + * @param {Object} [config={}] - Configuration object passed to Haro constructor + * @returns {Haro} New Haro instance configured and optionally populated + * @example + * const store = haro([ + * {id: 1, name: 'John', age: 30}, + * {id: 2, name: 'Jane', age: 25} + * ], { + * index: ['name', 'age'], + * versioning: true + * }); + */ function haro (data = null, config = {}) { const obj = new Haro(config); diff --git a/dist/haro.min.js b/dist/haro.min.js index ba2c4d61..6bd0e2bd 100644 --- a/dist/haro.min.js +++ b/dist/haro.min.js @@ -1,5 +1,5 @@ /*! 2025 Jason Mulligan - @version 15.2.6 + @version 16.0.0 */ -const e="",t="function",s="Invalid function",r="records",i=[8,9,"a","b"];function n(){return(65536*(Math.random()+1)|0).toString(16).substring(1)}const h="object"==typeof crypto?crypto.randomUUID.bind(crypto):function(){return`${n()}${n()}-${n()}-4${n().slice(0,3)}-${i[Math.floor(4*Math.random())]}${n().slice(0,3)}-${n()}${n()}${n()}`};class a{constructor({delimiter:e="|",id:t=this.uuid(),index:s=[],key:r="id",versioning:i=!1}={}){return this.data=new Map,this.delimiter=e,this.id=t,this.index=Array.isArray(s)?[...s]:[],this.indexes=new Map,this.key=r,this.versions=new Map,this.versioning=i,Object.defineProperty(this,"registry",{enumerable:!0,get:()=>Array.from(this.data.keys())}),Object.defineProperty(this,"size",{enumerable:!0,get:()=>this.data.size}),this.reindex()}batch(e,t="set"){const s="del"===t?e=>this.del(e,!0):e=>this.set(null,e,!0,!0);return this.onbatch(this.beforeBatch(e,t).map(s),t)}beforeBatch(e,t=""){return e}beforeClear(){}beforeDelete(e="",t=!1){return[e,t]}beforeSet(e="",t=!1){return[e,t]}clear(){return this.beforeClear(),this.data.clear(),this.indexes.clear(),this.versions.clear(),this.reindex().onclear(),this}clone(e){return JSON.parse(JSON.stringify(e))}del(e="",t=!1){if(!this.data.has(e))throw new Error("Record not found");const s=this.get(e,!0);this.beforeDelete(e,t),this.delIndex(this.index,this.indexes,this.delimiter,e,s),this.data.delete(e),this.ondelete(e,t),this.versioning&&this.versions.delete(e)}delIndex(e,t,s,r,i){e.forEach((e=>{const n=t.get(e);if(!n)return;const h=e.includes(s)?this.indexKeys(e,s,i):Array.isArray(i[e])?i[e]:[i[e]];this.each(h,(e=>{if(n.has(e)){const t=n.get(e);t.delete(r),0===t.size&&n.delete(e)}}))}))}dump(e=r){let t;return t=e===r?Array.from(this.entries()):Array.from(this.indexes).map((e=>(e[1]=Array.from(e[1]).map((e=>(e[1]=Array.from(e[1]),e))),e))),t}each(e=[],t){for(const[s,r]of e.entries())t(r,s);return e}entries(){return this.data.entries()}find(e={},t=!1){const s=Object.keys(e).sort(((e,t)=>e.localeCompare(t))).join(this.delimiter),r=this.indexes.get(s)??new Map;let i=[];if(r.size>0){const n=this.indexKeys(s,this.delimiter,e);i=Array.from(n.reduce(((e,t)=>(r.has(t)&&r.get(t).forEach((t=>e.add(t))),e)),new Set)).map((e=>this.get(e,t)))}return t?i:this.list(...i)}filter(e,r=!1){if(typeof e!==t)throw new Error(s);const i=r?(e,t)=>t:(e,t)=>Object.freeze([e,Object.freeze(t)]),n=this.reduce(((t,s,r,n)=>(e.call(n,s)&&t.push(i(r,s)),t)),[]);return r?n:Object.freeze(n)}forEach(e,t){return this.data.forEach(((t,s)=>e(this.clone(t),this.clone(s))),t??this.data),this}get(e,t=!1){const s=this.clone(this.data.get(e)??null);return t?s:this.list(e,s)}has(e){return this.data.has(e)}indexKeys(e="",t="|",s={}){return e.split(t).reduce(((e,r,i)=>{const n=[];return(Array.isArray(s[r])?s[r]:[s[r]]).forEach((s=>0===i?n.push(s):e.forEach((e=>n.push(`${e}${t}${s}`))))),n}),[])}keys(){return this.data.keys()}limit(e=0,t=0,s=!1){const r=this.registry.slice(e,e+t).map((e=>this.get(e,s)));return s?r:this.list(...r)}list(...e){return Object.freeze(e.map((e=>Object.freeze(e))))}map(e,r=!1){if(typeof e!==t)throw new Error(s);const i=[];return this.forEach(((t,s)=>i.push(e(t,s)))),r?i:this.list(...i)}merge(e,t,s=!1){return Array.isArray(e)&&Array.isArray(t)?e=s?t:e.concat(t):"object"==typeof e&&null!==e&&"object"==typeof t&&null!==t?this.each(Object.keys(t),(r=>{e[r]=this.merge(e[r],t[r],s)})):e=t,e}onbatch(e,t=""){return e}onclear(){}ondelete(e="",t=!1){return[e,t]}onoverride(e=""){return e}onset(e={},t=!1){return[e,t]}override(e,t=r){if("indexes"===t)this.indexes=new Map(e.map((e=>[e[0],new Map(e[1].map((e=>[e[0],new Set(e[1])])))])));else{if(t!==r)throw new Error("Invalid type");this.indexes.clear(),this.data=new Map(e)}return this.onoverride(t),!0}reduce(e,t,s=!1){let r=t??this.data.keys().next().value;return this.forEach(((t,i)=>{r=e(r,t,i,this,s)}),this),r}reindex(e){const t=e?[e]:this.index;return e&&!1===this.index.includes(e)&&this.index.push(e),this.each(t,(e=>this.indexes.set(e,new Map))),this.forEach(((e,s)=>this.each(t,(t=>this.setIndex(this.index,this.indexes,this.delimiter,s,e,t))))),this}search(e,s,r=!1){const i=new Map,n=typeof e===t,h=e&&typeof e.test===t;return e&&this.each(s?Array.isArray(s)?s:[s]:this.index,(t=>{let s=this.indexes.get(t);s&&s.forEach(((s,a)=>{switch(!0){case n&&e(a,t):case h&&e.test(Array.isArray(a)?a.join(","):a):case a===e:s.forEach((e=>{!1===i.has(e)&&this.data.has(e)&&i.set(e,this.get(e,r))}))}}))})),r?Array.from(i.values()):this.list(...Array.from(i.values()))}set(e=null,t={},s=!1,r=!1){null===e&&(e=t[this.key]??this.uuid());let i={...t,[this.key]:e};if(this.beforeSet(e,i,s,r),this.data.has(e)){const t=this.get(e,!0);this.delIndex(this.index,this.indexes,this.delimiter,e,t),this.versioning&&this.versions.get(e).add(Object.freeze(this.clone(t))),r||(i=this.merge(this.clone(t),i))}else this.versioning&&this.versions.set(e,new Set);this.data.set(e,i),this.setIndex(this.index,this.indexes,this.delimiter,e,i,null);const n=this.get(e);return this.onset(n,s),n}setIndex(e,t,s,r,i,n){this.each(null===n?e:[n],(e=>{let n=t.get(e);n||(n=new Map,t.set(e,n)),e.includes(s)?this.each(this.indexKeys(e,s,i),(e=>{n.has(e)||n.set(e,new Set),n.get(e).add(r)})):this.each(Array.isArray(i[e])?i[e]:[i[e]],(e=>{n.has(e)||n.set(e,new Set),n.get(e).add(r)}))}))}sort(e,t=!0){return t?Object.freeze(this.limit(0,this.data.size,!0).sort(e).map((e=>Object.freeze(e)))):this.limit(0,this.data.size,!0).sort(e)}sortBy(t="",s=!1){if(t===e)throw new Error("Invalid field");const r=[],i=[];!1===this.indexes.has(t)&&this.reindex(t);const n=this.indexes.get(t);return n.forEach(((e,t)=>i.push(t))),this.each(i.sort(),(e=>n.get(e).forEach((e=>r.push(this.get(e,s)))))),s?r:this.list(...r)}toArray(e=!0){const t=Array.from(this.data.values());return e&&(this.each(t,(e=>Object.freeze(e))),Object.freeze(t)),t}uuid(){return h()}values(){return this.data.values()}where(e={},t=!1,s="||"){const r=this.index.filter((t=>t in e));return 0===r.length?[]:this.filter((t=>r.map((r=>{const i=e[r],n=t[r];return Array.isArray(i)?Array.isArray(n)?"&&"===s?i.every((e=>n.includes(e))):i.some((e=>n.includes(e))):"&&"===s?i.every((e=>n===e)):i.some((e=>n===e)):i instanceof RegExp?Array.isArray(n)?"&&"===s?n.every((e=>i.test(e))):n.some((e=>i.test(e))):i.test(n):Array.isArray(n)?n.includes(i):n===i})).every(Boolean)),t)}}function o(e=null,t={}){const s=new a(t);return Array.isArray(e)&&s.batch(e,"set"),s}export{a as Haro,o as haro};//# sourceMappingURL=haro.min.js.map +import{randomUUID as e}from"crypto";const t="",s="&&",r="function",i="object",n="records",h="string",a="number",o="Invalid function";class l{constructor({delimiter:e="|",id:t=this.uuid(),immutable:s=!1,index:r=[],key:i="id",versioning:n=!1}={}){return this.data=new Map,this.delimiter=e,this.id=t,this.immutable=s,this.index=Array.isArray(r)?[...r]:[],this.indexes=new Map,this.key=i,this.versions=new Map,this.versioning=n,Object.defineProperty(this,"registry",{enumerable:!0,get:()=>Array.from(this.data.keys())}),Object.defineProperty(this,"size",{enumerable:!0,get:()=>this.data.size}),this.reindex()}batch(e,t="set"){const s="del"===t?e=>this.delete(e,!0):e=>this.set(null,e,!0,!0);return this.onbatch(this.beforeBatch(e,t).map(s),t)}beforeBatch(e,t=""){return e}beforeClear(){}beforeDelete(e="",t=!1){}beforeSet(e="",t={},s=!1,r=!1){}clear(){return this.beforeClear(),this.data.clear(),this.indexes.clear(),this.versions.clear(),this.reindex().onclear(),this}clone(e){return structuredClone(e)}delete(e="",t=!1){if(!this.data.has(e))throw new Error("Record not found");const s=this.get(e,!0);this.beforeDelete(e,t),this.deleteIndex(e,s),this.data.delete(e),this.ondelete(e,t),this.versioning&&this.versions.delete(e)}deleteIndex(e,t){return this.index.forEach(s=>{const r=this.indexes.get(s);if(!r)return;const i=s.includes(this.delimiter)?this.indexKeys(s,this.delimiter,t):Array.isArray(t[s])?t[s]:[t[s]];this.each(i,t=>{if(r.has(t)){const s=r.get(t);s.delete(e),0===s.size&&r.delete(t)}})}),this}dump(e=n){let t;return t=e===n?Array.from(this.entries()):Array.from(this.indexes).map(e=>(e[1]=Array.from(e[1]).map(e=>(e[1]=Array.from(e[1]),e)),e)),t}each(e=[],t){const s=e.length;for(let r=0;r0){const n=this.indexKeys(s,this.delimiter,e);i=Array.from(n.reduce((e,t)=>(r.has(t)&&r.get(t).forEach(t=>e.add(t)),e),new Set)).map(e=>this.get(e,t))}return!t&&this.immutable&&(i=Object.freeze(i)),i}filter(e,t=!1){if(typeof e!==r)throw new Error(o);let s=this.reduce((t,s)=>(e(s)&&t.push(s),t),[]);return t||(s=s.map(e=>this.list(e)),this.immutable&&(s=Object.freeze(s))),s}forEach(e,t=this){return this.data.forEach((s,r)=>{this.immutable&&(s=this.clone(s)),e.call(t,s,r)},this),this}freeze(...e){return Object.freeze(e.map(e=>Object.freeze(e)))}get(e,t=!1){let s=this.data.get(e)??null;return null===s||t||(s=this.list(s),this.immutable&&(s=Object.freeze(s))),s}has(e){return this.data.has(e)}indexKeys(e="",t="|",s={}){const r=e.split(t).sort(this.sortKeys),i=r.length;let n=[""];for(let e=0;ethis.get(e,s));return!s&&this.immutable&&(r=Object.freeze(r)),r}list(e){const t=[e[this.key],e];return this.immutable?this.freeze(...t):t}map(e,t=!1){if(typeof e!==r)throw new Error(o);let s=[];return this.forEach((t,r)=>s.push(e(t,r))),t||(s=s.map(e=>this.list(e)),this.immutable&&(s=Object.freeze(s))),s}merge(e,t,s=!1){return Array.isArray(e)&&Array.isArray(t)?e=s?t:e.concat(t):typeof e===i&&null!==e&&typeof t===i&&null!==t?this.each(Object.keys(t),r=>{e[r]=this.merge(e[r],t[r],s)}):e=t,e}onbatch(e,t=""){return e}onclear(){}ondelete(e="",t=!1){}onoverride(e=""){}onset(e={},t=!1){}override(e,t=n){if("indexes"===t)this.indexes=new Map(e.map(e=>[e[0],new Map(e[1].map(e=>[e[0],new Set(e[1])]))]));else{if(t!==n)throw new Error("Invalid type");this.indexes.clear(),this.data=new Map(e)}return this.onoverride(t),!0}reduce(e,t=[]){let s=t;return this.forEach((t,r)=>{s=e(s,t,r,this)},this),s}reindex(e){const t=e?[e]:this.index;return e&&!1===this.index.includes(e)&&this.index.push(e),this.each(t,e=>this.indexes.set(e,new Map)),this.forEach((e,s)=>this.each(t,t=>this.setIndex(s,e,t))),this}search(e,t,s=!1){const i=new Set,n=typeof e===r,h=e&&typeof e.test===r;if(!e)return this.immutable?this.freeze():[];const a=t?Array.isArray(t)?t:[t]:this.index;for(const t of a){const s=this.indexes.get(t);if(s)for(const[r,a]of s){let s=!1;if(s=n?e(r,t):h?e.test(Array.isArray(r)?r.join(","):r):r===e,s)for(const e of a)this.data.has(e)&&i.add(e)}}let o=Array.from(i).map(e=>this.get(e,s));return!s&&this.immutable&&(o=Object.freeze(o)),o}set(e=null,t={},s=!1,r=!1){null===e&&(e=t[this.key]??this.uuid());let i={...t,[this.key]:e};if(this.beforeSet(e,i,s,r),this.data.has(e)){const t=this.get(e,!0);this.deleteIndex(e,t),this.versioning&&this.versions.get(e).add(Object.freeze(this.clone(t))),r||(i=this.merge(this.clone(t),i))}else this.versioning&&this.versions.set(e,new Set);this.data.set(e,i),this.setIndex(e,i,null);const n=this.get(e);return this.onset(n,s),n}setIndex(e,t,s){return this.each(null===s?this.index:[s],s=>{let r=this.indexes.get(s);r||(r=new Map,this.indexes.set(s,r));const i=t=>{r.has(t)||r.set(t,new Set),r.get(t).add(e)};s.includes(this.delimiter)?this.each(this.indexKeys(s,this.delimiter,t),i):this.each(Array.isArray(t[s])?t[s]:[t[s]],i)}),this}sort(e,t=!1){const s=this.data.size;let r=this.limit(0,s,!0).sort(e);return t&&(r=this.freeze(...r)),r}sortKeys(e,t){return typeof e===h&&typeof t===h?e.localeCompare(t):typeof e===a&&typeof t===a?e-t:String(e).localeCompare(String(t))}sortBy(e="",s=!1){if(e===t)throw new Error("Invalid field");let r=[];const i=[];!1===this.indexes.has(e)&&this.reindex(e);const n=this.indexes.get(e);return n.forEach((e,t)=>i.push(t)),this.each(i.sort(this.sortKeys),e=>n.get(e).forEach(e=>r.push(this.get(e,s)))),this.immutable&&(r=Object.freeze(r)),r}toArray(){const e=Array.from(this.data.values());return this.immutable&&(this.each(e,e=>Object.freeze(e)),Object.freeze(e)),e}uuid(){return e()}values(){return this.data.values()}matchesPredicate(e,t,r){return Object.keys(t).every(i=>{const n=t[i],h=e[i];return Array.isArray(n)?Array.isArray(h)?r===s?n.every(e=>h.includes(e)):n.some(e=>h.includes(e)):r===s?n.every(e=>h===e):n.some(e=>h===e):n instanceof RegExp?Array.isArray(h)?r===s?h.every(e=>n.test(e)):h.some(e=>n.test(e)):n.test(h):Array.isArray(h)?h.includes(n):h===n})}where(e={},t="||"){const s=this.index.filter(t=>t in e);if(0===s.length)return[];const r=s.filter(e=>this.indexes.has(e));if(r.length>0){let s=new Set,i=!0;for(const t of r){const r=e[t],n=this.indexes.get(t),h=new Set;if(Array.isArray(r)){for(const e of r)if(n.has(e))for(const t of n.get(e))h.add(t)}else if(n.has(r))for(const e of n.get(r))h.add(e);i?(s=h,i=!1):s=new Set([...s].filter(e=>h.has(e)))}const n=[];for(const r of s){const s=this.get(r,!0);this.matchesPredicate(s,e,t)&&n.push(this.immutable?this.get(r):s)}return this.immutable?this.freeze(...n):n}return this.filter(s=>this.matchesPredicate(s,e,t))}}function c(e=null,t={}){const s=new l(t);return Array.isArray(e)&&s.batch(e,"set"),s}export{l as Haro,c as haro};//# sourceMappingURL=haro.min.js.map diff --git a/dist/haro.min.js.map b/dist/haro.min.js.map index 70e6d90e..5447b886 100644 --- a/dist/haro.min.js.map +++ b/dist/haro.min.js.map @@ -1 +1 @@ -{"version":3,"file":"haro.min.js","sources":["../src/constants.js","../src/uuid.js","../src/haro.js"],"sourcesContent":["export const STRING_COMMA = \",\";\nexport const STRING_EMPTY = \"\";\nexport const STRING_PIPE = \"|\";\nexport const STRING_DOUBLE_PIPE = \"||\";\nexport const STRING_A = \"a\";\nexport const STRING_B = \"b\";\nexport const STRING_DEL = \"del\";\nexport const STRING_FUNCTION = \"function\";\nexport const STRING_INDEXES = \"indexes\";\nexport const STRING_INVALID_FIELD = \"Invalid field\";\nexport const STRING_INVALID_FUNCTION = \"Invalid function\";\nexport const STRING_INVALID_TYPE = \"Invalid type\";\nexport const STRING_OBJECT = \"object\";\nexport const STRING_RECORD_NOT_FOUND = \"Record not found\";\nexport const STRING_RECORDS = \"records\";\nexport const STRING_REGISTRY = \"registry\";\nexport const STRING_SET = \"set\";\nexport const STRING_SIZE = \"size\";\nexport const INT_0 = 0;\nexport const INT_1 = 1;\nexport const INT_3 = 3;\nexport const INT_4 = 4;\nexport const INT_8 = 8;\nexport const INT_9 = 9;\nexport const INT_16 = 16;\n","import {INT_0, INT_1, INT_16, INT_3, INT_4, INT_8, INT_9, STRING_A, STRING_B, STRING_OBJECT} from \"./constants.js\";\n\n/* istanbul ignore next */\nconst r = [INT_8, INT_9, STRING_A, STRING_B];\n\n/* istanbul ignore next */\nfunction s () {\n\treturn ((Math.random() + INT_1) * 0x10000 | INT_0).toString(INT_16).substring(INT_1);\n}\n\n/* istanbul ignore next */\nfunction randomUUID () {\n\treturn `${s()}${s()}-${s()}-4${s().slice(INT_0, INT_3)}-${r[Math.floor(Math.random() * INT_4)]}${s().slice(INT_0, INT_3)}-${s()}${s()}${s()}`;\n}\n\nexport const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : randomUUID;\n","import {uuid} from \"./uuid.js\";\nimport {\n\tINT_0,\n\tSTRING_COMMA,\n\tSTRING_DEL,\n\tSTRING_DOUBLE_PIPE,\n\tSTRING_EMPTY,\n\tSTRING_FUNCTION,\n\tSTRING_INDEXES,\n\tSTRING_INVALID_FIELD,\n\tSTRING_INVALID_FUNCTION,\n\tSTRING_INVALID_TYPE,\n\tSTRING_PIPE,\n\tSTRING_RECORD_NOT_FOUND,\n\tSTRING_RECORDS,\n\tSTRING_REGISTRY,\n\tSTRING_SET,\n\tSTRING_SIZE\n} from \"./constants.js\";\n\nexport class Haro {\n\tconstructor ({delimiter = STRING_PIPE, id = this.uuid(), index = [], key = \"id\", versioning = false} = {}) {\n\t\tthis.data = new Map();\n\t\tthis.delimiter = delimiter;\n\t\tthis.id = id;\n\t\tthis.index = Array.isArray(index) ? [...index] : [];\n\t\tthis.indexes = new Map();\n\t\tthis.key = key;\n\t\tthis.versions = new Map();\n\t\tthis.versioning = versioning;\n\n\t\tObject.defineProperty(this, STRING_REGISTRY, {\n\t\t\tenumerable: true,\n\t\t\tget: () => Array.from(this.data.keys())\n\t\t});\n\t\tObject.defineProperty(this, STRING_SIZE, {\n\t\t\tenumerable: true,\n\t\t\tget: () => this.data.size\n\t\t});\n\n\t\treturn this.reindex();\n\t}\n\n\tbatch (args, type = STRING_SET) {\n\t\tconst fn = type === STRING_DEL ? i => this.del(i, true) : i => this.set(null, i, true, true);\n\n\t\treturn this.onbatch(this.beforeBatch(args, type).map(fn), type);\n\t}\n\n\tbeforeBatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\treturn arg;\n\t}\n\n\tbeforeClear () {\n\t\t// Hook for custom logic before clear; override in subclass if needed\n\t}\n\n\tbeforeDelete (key = STRING_EMPTY, batch = false) {\n\t\treturn [key, batch];\n\t}\n\n\tbeforeSet (key = STRING_EMPTY, batch = false) {\n\t\treturn [key, batch];\n\t}\n\n\tclear () {\n\t\tthis.beforeClear();\n\t\tthis.data.clear();\n\t\tthis.indexes.clear();\n\t\tthis.versions.clear();\n\t\tthis.reindex().onclear();\n\n\t\treturn this;\n\t}\n\n\tclone (arg) {\n\t\treturn JSON.parse(JSON.stringify(arg));\n\t}\n\n\tdel (key = STRING_EMPTY, batch = false) {\n\t\tif (!this.data.has(key)) {\n\t\t\tthrow new Error(STRING_RECORD_NOT_FOUND);\n\t\t}\n\t\tconst og = this.get(key, true);\n\t\tthis.beforeDelete(key, batch);\n\t\tthis.delIndex(this.index, this.indexes, this.delimiter, key, og);\n\t\tthis.data.delete(key);\n\t\tthis.ondelete(key, batch);\n\t\tif (this.versioning) {\n\t\t\tthis.versions.delete(key);\n\t\t}\n\t}\n\n\tdelIndex (index, indexes, delimiter, key, data) {\n\t\tindex.forEach(i => {\n\t\t\tconst idx = indexes.get(i);\n\t\t\tif (!idx) return;\n\t\t\tconst values = i.includes(delimiter) ?\n\t\t\t\tthis.indexKeys(i, delimiter, data) :\n\t\t\t\tArray.isArray(data[i]) ? data[i] : [data[i]];\n\t\t\tthis.each(values, value => {\n\t\t\t\tif (idx.has(value)) {\n\t\t\t\t\tconst o = idx.get(value);\n\t\t\t\t\to.delete(key);\n\t\t\t\t\tif (o.size === INT_0) {\n\t\t\t\t\t\tidx.delete(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\tdump (type = STRING_RECORDS) {\n\t\tlet result;\n\n\t\tif (type === STRING_RECORDS) {\n\t\t\tresult = Array.from(this.entries());\n\t\t} else {\n\t\t\tresult = Array.from(this.indexes).map(i => {\n\t\t\t\ti[1] = Array.from(i[1]).map(ii => {\n\t\t\t\t\tii[1] = Array.from(ii[1]);\n\n\t\t\t\t\treturn ii;\n\t\t\t\t});\n\n\t\t\t\treturn i;\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\teach (arr = [], fn) {\n\t\tfor (const [idx, value] of arr.entries()) {\n\t\t\tfn(value, idx);\n\t\t}\n\n\t\treturn arr;\n\t}\n\n\tentries () {\n\t\treturn this.data.entries();\n\t}\n\n\tfind (where = {}, raw = false) {\n\t\tconst key = Object.keys(where).sort((a, b) => a.localeCompare(b)).join(this.delimiter);\n\t\tconst index = this.indexes.get(key) ?? new Map();\n\t\tlet result = [];\n\t\tif (index.size > 0) {\n\t\t\tconst keys = this.indexKeys(key, this.delimiter, where);\n\t\t\tresult = Array.from(keys.reduce((a, v) => {\n\t\t\t\tif (index.has(v)) {\n\t\t\t\t\tindex.get(v).forEach(k => a.add(k));\n\t\t\t\t}\n\n\t\t\t\treturn a;\n\t\t\t}, new Set())).map(i => this.get(i, raw));\n\t\t}\n\n\t\treturn raw ? result : this.list(...result);\n\t}\n\n\tfilter (fn, raw = false) {\n\t\tif (typeof fn !== STRING_FUNCTION) {\n\t\t\tthrow new Error(STRING_INVALID_FUNCTION);\n\t\t}\n\t\tconst x = raw ? (k, v) => v : (k, v) => Object.freeze([k, Object.freeze(v)]);\n\t\tconst result = this.reduce((a, v, k, ctx) => {\n\t\t\tif (fn.call(ctx, v)) {\n\t\t\t\ta.push(x(k, v));\n\t\t\t}\n\n\t\t\treturn a;\n\t\t}, []);\n\n\t\treturn raw ? result : Object.freeze(result);\n\t}\n\n\tforEach (fn, ctx) {\n\t\tthis.data.forEach((value, key) => fn(this.clone(value), this.clone(key)), ctx ?? this.data);\n\n\t\treturn this;\n\t}\n\n\tget (key, raw = false) {\n\t\tconst result = this.clone(this.data.get(key) ?? null);\n\n\t\treturn raw ? result : this.list(key, result);\n\t}\n\n\thas (key) {\n\t\treturn this.data.has(key);\n\t}\n\n\tindexKeys (arg = STRING_EMPTY, delimiter = STRING_PIPE, data = {}) {\n\t\treturn arg.split(delimiter).reduce((a, li, lidx) => {\n\t\t\tconst result = [];\n\n\t\t\t(Array.isArray(data[li]) ? data[li] : [data[li]]).forEach(lli => lidx === INT_0 ? result.push(lli) : a.forEach(x => result.push(`${x}${delimiter}${lli}`)));\n\n\t\t\treturn result;\n\t\t}, []);\n\t}\n\n\tkeys () {\n\t\treturn this.data.keys();\n\t}\n\n\tlimit (offset = INT_0, max = INT_0, raw = false) {\n\t\tconst result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw));\n\n\t\treturn raw ? result : this.list(...result);\n\t}\n\n\tlist (...args) {\n\t\treturn Object.freeze(args.map(i => Object.freeze(i)));\n\t}\n\n\tmap (fn, raw = false) {\n\t\tif (typeof fn !== STRING_FUNCTION) {\n\t\t\tthrow new Error(STRING_INVALID_FUNCTION);\n\t\t}\n\n\t\tconst result = [];\n\n\t\tthis.forEach((value, key) => result.push(fn(value, key)));\n\n\t\treturn raw ? result : this.list(...result);\n\t}\n\n\tmerge (a, b, override = false) {\n\t\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\t\ta = override ? b : a.concat(b);\n\t\t} else if (typeof a === \"object\" && a !== null && typeof b === \"object\" && b !== null) {\n\t\t\tthis.each(Object.keys(b), i => {\n\t\t\t\ta[i] = this.merge(a[i], b[i], override);\n\t\t\t});\n\t\t} else {\n\t\t\ta = b;\n\t\t}\n\n\t\treturn a;\n\t}\n\n\tonbatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\treturn arg;\n\t}\n\n\tonclear () {\n\t\t// Hook for custom logic after clear; override in subclass if needed\n\t}\n\n\tondelete (key = STRING_EMPTY, batch = false) {\n\t\treturn [key, batch];\n\t}\n\n\tonoverride (type = STRING_EMPTY) {\n\t\treturn type;\n\t}\n\n\tonset (arg = {}, batch = false) {\n\t\treturn [arg, batch];\n\t}\n\n\toverride (data, type = STRING_RECORDS) {\n\t\tconst result = true;\n\n\t\tif (type === STRING_INDEXES) {\n\t\t\tthis.indexes = new Map(data.map(i => [i[0], new Map(i[1].map(ii => [ii[0], new Set(ii[1])]))]));\n\t\t} else if (type === STRING_RECORDS) {\n\t\t\tthis.indexes.clear();\n\t\t\tthis.data = new Map(data);\n\t\t} else {\n\t\t\tthrow new Error(STRING_INVALID_TYPE);\n\t\t}\n\n\t\tthis.onoverride(type);\n\n\t\treturn result;\n\t}\n\n\treduce (fn, accumulator, raw = false) {\n\t\tlet a = accumulator ?? this.data.keys().next().value;\n\n\t\tthis.forEach((v, k) => {\n\t\t\ta = fn(a, v, k, this, raw);\n\t\t}, this);\n\n\t\treturn a;\n\t}\n\n\treindex (index) {\n\t\tconst indices = index ? [index] : this.index;\n\n\t\tif (index && this.index.includes(index) === false) {\n\t\t\tthis.index.push(index);\n\t\t}\n\n\t\tthis.each(indices, i => this.indexes.set(i, new Map()));\n\t\tthis.forEach((data, key) => this.each(indices, i => this.setIndex(this.index, this.indexes, this.delimiter, key, data, i)));\n\n\t\treturn this;\n\t}\n\n\tsearch (value, index, raw = false) {\n\t\tconst result = new Map(),\n\t\t\tfn = typeof value === STRING_FUNCTION,\n\t\t\trgex = value && typeof value.test === STRING_FUNCTION;\n\n\t\tif (value) {\n\t\t\tthis.each(index ? Array.isArray(index) ? index : [index] : this.index, i => {\n\t\t\t\tlet idx = this.indexes.get(i);\n\n\t\t\t\tif (idx) {\n\t\t\t\t\tidx.forEach((lset, lkey) => {\n\t\t\t\t\t\tswitch (true) {\n\t\t\t\t\t\t\tcase fn && value(lkey, i):\n\t\t\t\t\t\t\tcase rgex && value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey):\n\t\t\t\t\t\t\tcase lkey === value:\n\t\t\t\t\t\t\t\tlset.forEach(key => {\n\t\t\t\t\t\t\t\t\tif (result.has(key) === false && this.data.has(key)) {\n\t\t\t\t\t\t\t\t\t\tresult.set(key, this.get(key, raw));\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tvoid 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\treturn raw ? Array.from(result.values()) : this.list(...Array.from(result.values()));\n\t}\n\n\tset (key = null, data = {}, batch = false, override = false) {\n\t\tif (key === null) {\n\t\t\tkey = data[this.key] ?? this.uuid();\n\t\t}\n\t\tlet x = {...data, [this.key]: key};\n\t\tthis.beforeSet(key, x, batch, override);\n\t\tif (!this.data.has(key)) {\n\t\t\tif (this.versioning) {\n\t\t\t\tthis.versions.set(key, new Set());\n\t\t\t}\n\t\t} else {\n\t\t\tconst og = this.get(key, true);\n\t\t\tthis.delIndex(this.index, this.indexes, this.delimiter, key, og);\n\t\t\tif (this.versioning) {\n\t\t\t\tthis.versions.get(key).add(Object.freeze(this.clone(og)));\n\t\t\t}\n\t\t\tif (!override) {\n\t\t\t\tx = this.merge(this.clone(og), x);\n\t\t\t}\n\t\t}\n\t\tthis.data.set(key, x);\n\t\tthis.setIndex(this.index, this.indexes, this.delimiter, key, x, null);\n\t\tconst result = this.get(key);\n\t\tthis.onset(result, batch);\n\n\t\treturn result;\n\t}\n\n\tsetIndex (index, indexes, delimiter, key, data, indice) {\n\t\tthis.each(indice === null ? index : [indice], i => {\n\t\t\tlet lindex = indexes.get(i);\n\t\t\tif (!lindex) {\n\t\t\t\tlindex = new Map();\n\t\t\t\tindexes.set(i, lindex);\n\t\t\t}\n\t\t\tif (i.includes(delimiter)) {\n\t\t\t\tthis.each(this.indexKeys(i, delimiter, data), c => {\n\t\t\t\t\tif (!lindex.has(c)) {\n\t\t\t\t\t\tlindex.set(c, new Set());\n\t\t\t\t\t}\n\t\t\t\t\tlindex.get(c).add(key);\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tthis.each(Array.isArray(data[i]) ? data[i] : [data[i]], d => {\n\t\t\t\t\tif (!lindex.has(d)) {\n\t\t\t\t\t\tlindex.set(d, new Set());\n\t\t\t\t\t}\n\t\t\t\t\tlindex.get(d).add(key);\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t}\n\n\tsort (fn, frozen = true) {\n\t\treturn frozen ? Object.freeze(this.limit(INT_0, this.data.size, true).sort(fn).map(i => Object.freeze(i))) : this.limit(INT_0, this.data.size, true).sort(fn);\n\t}\n\n\tsortBy (index = STRING_EMPTY, raw = false) {\n\t\tif (index === STRING_EMPTY) {\n\t\t\tthrow new Error(STRING_INVALID_FIELD);\n\t\t}\n\n\t\tconst result = [],\n\t\t\tkeys = [];\n\n\t\tif (this.indexes.has(index) === false) {\n\t\t\tthis.reindex(index);\n\t\t}\n\n\t\tconst lindex = this.indexes.get(index);\n\n\t\tlindex.forEach((idx, key) => keys.push(key));\n\t\tthis.each(keys.sort(), i => lindex.get(i).forEach(key => result.push(this.get(key, raw))));\n\n\t\treturn raw ? result : this.list(...result);\n\t}\n\n\ttoArray (frozen = true) {\n\t\tconst result = Array.from(this.data.values());\n\n\t\tif (frozen) {\n\t\t\tthis.each(result, i => Object.freeze(i));\n\t\t\tObject.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tuuid () {\n\t\treturn uuid();\n\t}\n\n\tvalues () {\n\t\treturn this.data.values();\n\t}\n\n\twhere (predicate = {}, raw = false, op = STRING_DOUBLE_PIPE) {\n\t\tconst keys = this.index.filter(i => i in predicate);\n\n\t\tif (keys.length === 0) return [];\n\n\t\t// Supported operators: '||' (OR), '&&' (AND)\n\t\t// Always AND across fields (all keys must match for a record)\n\t\treturn this.filter(a => {\n\t\t\tconst matches = keys.map(i => {\n\t\t\t\tconst pred = predicate[i];\n\t\t\t\tconst val = a[i];\n\t\t\t\tif (Array.isArray(pred)) {\n\t\t\t\t\tif (Array.isArray(val)) {\n\t\t\t\t\t\tif (op === \"&&\") {\n\t\t\t\t\t\t\treturn pred.every(p => val.includes(p));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn pred.some(p => val.includes(p));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (op === \"&&\") {\n\t\t\t\t\t\treturn pred.every(p => val === p);\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn pred.some(p => val === p);\n\t\t\t\t\t}\n\t\t\t\t} else if (pred instanceof RegExp) {\n\t\t\t\t\tif (Array.isArray(val)) {\n\t\t\t\t\t\tif (op === \"&&\") {\n\t\t\t\t\t\t\treturn val.every(v => pred.test(v));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn val.some(v => pred.test(v));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn pred.test(val);\n\t\t\t\t\t}\n\t\t\t\t} else if (Array.isArray(val)) {\n\t\t\t\t\treturn val.includes(pred);\n\t\t\t\t} else {\n\t\t\t\t\treturn val === pred;\n\t\t\t\t}\n\t\t\t});\n\t\t\tconst isMatch = matches.every(Boolean);\n\n\t\t\treturn isMatch;\n\t\t}, raw);\n\t}\n\n}\n\nexport function haro (data = null, config = {}) {\n\tconst obj = new Haro(config);\n\n\tif (Array.isArray(data)) {\n\t\tobj.batch(data, STRING_SET);\n\t}\n\n\treturn obj;\n}\n"],"names":["STRING_EMPTY","STRING_FUNCTION","STRING_INVALID_FUNCTION","STRING_RECORDS","r","s","Math","random","toString","substring","uuid","crypto","randomUUID","bind","slice","floor","Haro","constructor","delimiter","id","this","index","key","versioning","data","Map","Array","isArray","indexes","versions","Object","defineProperty","enumerable","get","from","keys","size","reindex","batch","args","type","fn","i","del","set","onbatch","beforeBatch","map","arg","beforeClear","beforeDelete","beforeSet","clear","onclear","clone","JSON","parse","stringify","has","Error","og","delIndex","delete","ondelete","forEach","idx","values","includes","indexKeys","each","value","o","dump","result","entries","ii","arr","find","where","raw","sort","a","b","localeCompare","join","reduce","v","k","add","Set","list","filter","x","freeze","ctx","call","push","split","li","lidx","lli","limit","offset","max","registry","merge","override","concat","onoverride","onset","accumulator","next","indices","setIndex","search","rgex","test","lset","lkey","indice","lindex","c","d","frozen","sortBy","toArray","predicate","op","length","pred","val","every","p","some","RegExp","Boolean","haro","config","obj"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"haro.min.js","sources":["../src/constants.js","../src/haro.js"],"sourcesContent":["// String constants - Single characters and symbols\nexport const STRING_COMMA = \",\";\nexport const STRING_EMPTY = \"\";\nexport const STRING_PIPE = \"|\";\nexport const STRING_DOUBLE_PIPE = \"||\";\nexport const STRING_DOUBLE_AND = \"&&\";\n\n// String constants - Operation and type names\nexport const STRING_ID = \"id\";\nexport const STRING_DEL = \"del\";\nexport const STRING_FUNCTION = \"function\";\nexport const STRING_INDEXES = \"indexes\";\nexport const STRING_OBJECT = \"object\";\nexport const STRING_RECORDS = \"records\";\nexport const STRING_REGISTRY = \"registry\";\nexport const STRING_SET = \"set\";\nexport const STRING_SIZE = \"size\";\nexport const STRING_STRING = \"string\";\nexport const STRING_NUMBER = \"number\";\n\n// String constants - Error messages\nexport const STRING_INVALID_FIELD = \"Invalid field\";\nexport const STRING_INVALID_FUNCTION = \"Invalid function\";\nexport const STRING_INVALID_TYPE = \"Invalid type\";\nexport const STRING_RECORD_NOT_FOUND = \"Record not found\";\n\n// Integer constants\nexport const INT_0 = 0;\n","import {randomUUID as uuid} from \"crypto\";\nimport {\n\tINT_0,\n\tSTRING_COMMA,\n\tSTRING_DEL, STRING_DOUBLE_AND,\n\tSTRING_DOUBLE_PIPE,\n\tSTRING_EMPTY,\n\tSTRING_FUNCTION,\n\tSTRING_ID,\n\tSTRING_INDEXES,\n\tSTRING_INVALID_FIELD,\n\tSTRING_INVALID_FUNCTION,\n\tSTRING_INVALID_TYPE, STRING_NUMBER, STRING_OBJECT,\n\tSTRING_PIPE,\n\tSTRING_RECORD_NOT_FOUND,\n\tSTRING_RECORDS,\n\tSTRING_REGISTRY,\n\tSTRING_SET,\n\tSTRING_SIZE, STRING_STRING\n} from \"./constants.js\";\n\n/**\n * Haro is a modern immutable DataStore for collections of records with indexing,\n * versioning, and batch operations support. It provides a Map-like interface\n * with advanced querying capabilities through indexes.\n * @class\n * @example\n * const store = new Haro({\n * index: ['name', 'age'],\n * key: 'id',\n * versioning: true\n * });\n *\n * store.set(null, {name: 'John', age: 30});\n * const results = store.find({name: 'John'});\n */\nexport class Haro {\n\t/**\n\t * Creates a new Haro instance with specified configuration\n\t * @param {Object} [config={}] - Configuration object for the store\n\t * @param {string} [config.delimiter=STRING_PIPE] - Delimiter for composite indexes (default: '|')\n\t * @param {string} [config.id] - Unique identifier for this instance (auto-generated if not provided)\n\t * @param {boolean} [config.immutable=false] - Return frozen/immutable objects for data safety\n\t * @param {string[]} [config.index=[]] - Array of field names to create indexes for\n\t * @param {string} [config.key=STRING_ID] - Primary key field name used for record identification\n\t * @param {boolean} [config.versioning=false] - Enable versioning to track record changes\n\t * @constructor\n\t * @example\n\t * const store = new Haro({\n\t * index: ['name', 'email', 'name|department'],\n\t * key: 'userId',\n\t * versioning: true,\n\t * immutable: true\n\t * });\n\t */\n\tconstructor ({delimiter = STRING_PIPE, id = this.uuid(), immutable = false, index = [], key = STRING_ID, versioning = false} = {}) {\n\t\tthis.data = new Map();\n\t\tthis.delimiter = delimiter;\n\t\tthis.id = id;\n\t\tthis.immutable = immutable;\n\t\tthis.index = Array.isArray(index) ? [...index] : [];\n\t\tthis.indexes = new Map();\n\t\tthis.key = key;\n\t\tthis.versions = new Map();\n\t\tthis.versioning = versioning;\n\t\tObject.defineProperty(this, STRING_REGISTRY, {\n\t\t\tenumerable: true,\n\t\t\tget: () => Array.from(this.data.keys())\n\t\t});\n\t\tObject.defineProperty(this, STRING_SIZE, {\n\t\t\tenumerable: true,\n\t\t\tget: () => this.data.size\n\t\t});\n\n\t\treturn this.reindex();\n\t}\n\n\t/**\n\t * Performs batch operations on multiple records for efficient bulk processing\n\t * @param {Array} args - Array of records to process\n\t * @param {string} [type=STRING_SET] - Type of operation: 'set' for upsert, 'del' for delete\n\t * @returns {Array} Array of results from the batch operation\n\t * @throws {Error} Throws error if individual operations fail during batch processing\n\t * @example\n\t * const results = store.batch([\n\t * {id: 1, name: 'John'},\n\t * {id: 2, name: 'Jane'}\n\t * ], 'set');\n\t */\n\tbatch (args, type = STRING_SET) {\n\t\tconst fn = type === STRING_DEL ? i => this.delete(i, true) : i => this.set(null, i, true, true);\n\n\t\treturn this.onbatch(this.beforeBatch(args, type).map(fn), type);\n\t}\n\n\t/**\n\t * Lifecycle hook executed before batch operations for custom preprocessing\n\t * @param {Array} arg - Arguments passed to batch operation\n\t * @param {string} [type=STRING_EMPTY] - Type of batch operation ('set' or 'del')\n\t * @returns {Array} The arguments array (possibly modified) to be processed\n\t */\n\tbeforeBatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic before batch; override in subclass if needed\n\t\treturn arg;\n\t}\n\n\t/**\n\t * Lifecycle hook executed before clear operation for custom preprocessing\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t * @example\n\t * class MyStore extends Haro {\n\t * beforeClear() {\n\t * this.backup = this.toArray();\n\t * }\n\t * }\n\t */\n\tbeforeClear () {\n\t\t// Hook for custom logic before clear; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed before delete operation for custom preprocessing\n\t * @param {string} [key=STRING_EMPTY] - Key of record to delete\n\t * @param {boolean} [batch=false] - Whether this is part of a batch operation\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tbeforeDelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic before delete; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed before set operation for custom preprocessing\n\t * @param {string} [key=STRING_EMPTY] - Key of record to set\n\t * @param {Object} [data={}] - Record data being set\n\t * @param {boolean} [batch=false] - Whether this is part of a batch operation\n\t * @param {boolean} [override=false] - Whether to override existing data\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tbeforeSet (key = STRING_EMPTY, data = {}, batch = false, override = false) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic before set; override in subclass if needed\n\t}\n\n\t/**\n\t * Removes all records, indexes, and versions from the store\n\t * @returns {Haro} This instance for method chaining\n\t * @example\n\t * store.clear();\n\t * console.log(store.size); // 0\n\t */\n\tclear () {\n\t\tthis.beforeClear();\n\t\tthis.data.clear();\n\t\tthis.indexes.clear();\n\t\tthis.versions.clear();\n\t\tthis.reindex().onclear();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Creates a deep clone of the given value, handling objects, arrays, and primitives\n\t * @param {*} arg - Value to clone (any type)\n\t * @returns {*} Deep clone of the argument\n\t * @example\n\t * const original = {name: 'John', tags: ['user', 'admin']};\n\t * const cloned = store.clone(original);\n\t * cloned.tags.push('new'); // original.tags is unchanged\n\t */\n\tclone (arg) {\n\t\treturn structuredClone(arg);\n\t}\n\n\t/**\n\t * Deletes a record from the store and removes it from all indexes\n\t * @param {string} [key=STRING_EMPTY] - Key of record to delete\n\t * @param {boolean} [batch=false] - Whether this is part of a batch operation\n\t * @returns {void}\n\t * @throws {Error} Throws error if record with the specified key is not found\n\t * @example\n\t * store.delete('user123');\n\t * // Throws error if 'user123' doesn't exist\n\t */\n\tdelete (key = STRING_EMPTY, batch = false) {\n\t\tif (!this.data.has(key)) {\n\t\t\tthrow new Error(STRING_RECORD_NOT_FOUND);\n\t\t}\n\t\tconst og = this.get(key, true);\n\t\tthis.beforeDelete(key, batch);\n\t\tthis.deleteIndex(key, og);\n\t\tthis.data.delete(key);\n\t\tthis.ondelete(key, batch);\n\t\tif (this.versioning) {\n\t\t\tthis.versions.delete(key);\n\t\t}\n\t}\n\n\t/**\n\t * Internal method to remove entries from indexes for a deleted record\n\t * @param {string} key - Key of record being deleted\n\t * @param {Object} data - Data of record being deleted\n\t * @returns {Haro} This instance for method chaining\n\t */\n\tdeleteIndex (key, data) {\n\t\tthis.index.forEach(i => {\n\t\t\tconst idx = this.indexes.get(i);\n\t\t\tif (!idx) return;\n\t\t\tconst values = i.includes(this.delimiter) ?\n\t\t\t\tthis.indexKeys(i, this.delimiter, data) :\n\t\t\t\tArray.isArray(data[i]) ? data[i] : [data[i]];\n\t\t\tthis.each(values, value => {\n\t\t\t\tif (idx.has(value)) {\n\t\t\t\t\tconst o = idx.get(value);\n\t\t\t\t\to.delete(key);\n\t\t\t\t\tif (o.size === INT_0) {\n\t\t\t\t\t\tidx.delete(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Exports complete store data or indexes for persistence or debugging\n\t * @param {string} [type=STRING_RECORDS] - Type of data to export: 'records' or 'indexes'\n\t * @returns {Array} Array of [key, value] pairs for records, or serialized index structure\n\t * @example\n\t * const records = store.dump('records');\n\t * const indexes = store.dump('indexes');\n\t */\n\tdump (type = STRING_RECORDS) {\n\t\tlet result;\n\t\tif (type === STRING_RECORDS) {\n\t\t\tresult = Array.from(this.entries());\n\t\t} else {\n\t\t\tresult = Array.from(this.indexes).map(i => {\n\t\t\t\ti[1] = Array.from(i[1]).map(ii => {\n\t\t\t\t\tii[1] = Array.from(ii[1]);\n\n\t\t\t\t\treturn ii;\n\t\t\t\t});\n\n\t\t\t\treturn i;\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Utility method to iterate over an array with a callback function\n\t * @param {Array<*>} [arr=[]] - Array to iterate over\n\t * @param {Function} fn - Function to call for each element (element, index)\n\t * @returns {Array<*>} The original array for method chaining\n\t * @example\n\t * store.each([1, 2, 3], (item, index) => console.log(item, index));\n\t */\n\teach (arr = [], fn) {\n\t\tconst len = arr.length;\n\t\tfor (let i = 0; i < len; i++) {\n\t\t\tfn(arr[i], i);\n\t\t}\n\n\t\treturn arr;\n\t}\n\n\t/**\n\t * Returns an iterator of [key, value] pairs for each record in the store\n\t * @returns {Iterator>} Iterator of [key, value] pairs\n\t * @example\n\t * for (const [key, value] of store.entries()) {\n\t * console.log(key, value);\n\t * }\n\t */\n\tentries () {\n\t\treturn this.data.entries();\n\t}\n\n\t/**\n\t * Finds records matching the specified criteria using indexes for optimal performance\n\t * @param {Object} [where={}] - Object with field-value pairs to match against\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of matching records (frozen if immutable mode)\n\t * @example\n\t * const users = store.find({department: 'engineering', active: true});\n\t * const admins = store.find({role: 'admin'});\n\t */\n\tfind (where = {}, raw = false) {\n\t\tconst key = Object.keys(where).sort(this.sortKeys).join(this.delimiter);\n\t\tconst index = this.indexes.get(key) ?? new Map();\n\t\tlet result = [];\n\t\tif (index.size > 0) {\n\t\t\tconst keys = this.indexKeys(key, this.delimiter, where);\n\t\t\tresult = Array.from(keys.reduce((a, v) => {\n\t\t\t\tif (index.has(v)) {\n\t\t\t\t\tindex.get(v).forEach(k => a.add(k));\n\t\t\t\t}\n\n\t\t\t\treturn a;\n\t\t\t}, new Set())).map(i => this.get(i, raw));\n\t\t}\n\t\tif (!raw && this.immutable) {\n\t\t\tresult = Object.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Filters records using a predicate function, similar to Array.filter\n\t * @param {Function} fn - Predicate function to test each record (record, key, store)\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of records that pass the predicate test\n\t * @throws {Error} Throws error if fn is not a function\n\t * @example\n\t * const adults = store.filter(record => record.age >= 18);\n\t * const recent = store.filter(record => record.created > Date.now() - 86400000);\n\t */\n\tfilter (fn, raw = false) {\n\t\tif (typeof fn !== STRING_FUNCTION) {\n\t\t\tthrow new Error(STRING_INVALID_FUNCTION);\n\t\t}\n\t\tlet result = this.reduce((a, v) => {\n\t\t\tif (fn(v)) {\n\t\t\t\ta.push(v);\n\t\t\t}\n\n\t\t\treturn a;\n\t\t}, []);\n\t\tif (!raw) {\n\t\t\tresult = result.map(i => this.list(i));\n\n\t\t\tif (this.immutable) {\n\t\t\t\tresult = Object.freeze(result);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Executes a function for each record in the store, similar to Array.forEach\n\t * @param {Function} fn - Function to execute for each record (value, key)\n\t * @param {*} [ctx] - Context object to use as 'this' when executing the function\n\t * @returns {Haro} This instance for method chaining\n\t * @example\n\t * store.forEach((record, key) => {\n\t * console.log(`${key}: ${record.name}`);\n\t * });\n\t */\n\tforEach (fn, ctx = this) {\n\t\tthis.data.forEach((value, key) => {\n\t\t\tif (this.immutable) {\n\t\t\t\tvalue = this.clone(value);\n\t\t\t}\n\t\t\tfn.call(ctx, value, key);\n\t\t}, this);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Creates a frozen array from the given arguments for immutable data handling\n\t * @param {...*} args - Arguments to freeze into an array\n\t * @returns {Array<*>} Frozen array containing frozen arguments\n\t * @example\n\t * const frozen = store.freeze(obj1, obj2, obj3);\n\t * // Returns Object.freeze([Object.freeze(obj1), Object.freeze(obj2), Object.freeze(obj3)])\n\t */\n\tfreeze (...args) {\n\t\treturn Object.freeze(args.map(i => Object.freeze(i)));\n\t}\n\n\t/**\n\t * Retrieves a record by its key\n\t * @param {string} key - Key of record to retrieve\n\t * @param {boolean} [raw=false] - Whether to return raw data (true) or processed/frozen data (false)\n\t * @returns {Object|null} The record if found, null if not found\n\t * @example\n\t * const user = store.get('user123');\n\t * const rawUser = store.get('user123', true);\n\t */\n\tget (key, raw = false) {\n\t\tlet result = this.data.get(key) ?? null;\n\t\tif (result !== null && !raw) {\n\t\t\tresult = this.list(result);\n\t\t\tif (this.immutable) {\n\t\t\t\tresult = Object.freeze(result);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Checks if a record with the specified key exists in the store\n\t * @param {string} key - Key to check for existence\n\t * @returns {boolean} True if record exists, false otherwise\n\t * @example\n\t * if (store.has('user123')) {\n\t * console.log('User exists');\n\t * }\n\t */\n\thas (key) {\n\t\treturn this.data.has(key);\n\t}\n\n\t/**\n\t * Generates index keys for composite indexes from data values\n\t * @param {string} [arg=STRING_EMPTY] - Composite index field names joined by delimiter\n\t * @param {string} [delimiter=STRING_PIPE] - Delimiter used in composite index\n\t * @param {Object} [data={}] - Data object to extract field values from\n\t * @returns {string[]} Array of generated index keys\n\t * @example\n\t * // For index 'name|department' with data {name: 'John', department: 'IT'}\n\t * const keys = store.indexKeys('name|department', '|', data);\n\t * // Returns ['John|IT']\n\t */\n\tindexKeys (arg = STRING_EMPTY, delimiter = STRING_PIPE, data = {}) {\n\t\tconst fields = arg.split(delimiter).sort(this.sortKeys);\n\t\tconst fieldsLen = fields.length;\n\t\tlet result = [\"\"];\n\t\tfor (let i = 0; i < fieldsLen; i++) {\n\t\t\tconst field = fields[i];\n\t\t\tconst values = Array.isArray(data[field]) ? data[field] : [data[field]];\n\t\t\tconst newResult = [];\n\t\t\tconst resultLen = result.length;\n\t\t\tconst valuesLen = values.length;\n\t\t\tfor (let j = 0; j < resultLen; j++) {\n\t\t\t\tfor (let k = 0; k < valuesLen; k++) {\n\t\t\t\t\tconst newKey = i === 0 ? values[k] : `${result[j]}${delimiter}${values[k]}`;\n\t\t\t\t\tnewResult.push(newKey);\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult = newResult;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Returns an iterator of all keys in the store\n\t * @returns {Iterator} Iterator of record keys\n\t * @example\n\t * for (const key of store.keys()) {\n\t * console.log(key);\n\t * }\n\t */\n\tkeys () {\n\t\treturn this.data.keys();\n\t}\n\n\t/**\n\t * Returns a limited subset of records with offset support for pagination\n\t * @param {number} [offset=INT_0] - Number of records to skip from the beginning\n\t * @param {number} [max=INT_0] - Maximum number of records to return\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of records within the specified range\n\t * @example\n\t * const page1 = store.limit(0, 10); // First 10 records\n\t * const page2 = store.limit(10, 10); // Next 10 records\n\t */\n\tlimit (offset = INT_0, max = INT_0, raw = false) {\n\t\tlet result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw));\n\t\tif (!raw && this.immutable) {\n\t\t\tresult = Object.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Converts a record into a [key, value] pair array format\n\t * @param {Object} arg - Record object to convert to list format\n\t * @returns {Array<*>} Array containing [key, record] where key is extracted from record's key field\n\t * @example\n\t * const record = {id: 'user123', name: 'John', age: 30};\n\t * const pair = store.list(record); // ['user123', {id: 'user123', name: 'John', age: 30}]\n\t */\n\tlist (arg) {\n\t\tconst result = [arg[this.key], arg];\n\n\t\treturn this.immutable ? this.freeze(...result) : result;\n\t}\n\n\t/**\n\t * Transforms all records using a mapping function, similar to Array.map\n\t * @param {Function} fn - Function to transform each record (record, key)\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array<*>} Array of transformed results\n\t * @throws {Error} Throws error if fn is not a function\n\t * @example\n\t * const names = store.map(record => record.name);\n\t * const summaries = store.map(record => ({id: record.id, name: record.name}));\n\t */\n\tmap (fn, raw = false) {\n\t\tif (typeof fn !== STRING_FUNCTION) {\n\t\t\tthrow new Error(STRING_INVALID_FUNCTION);\n\t\t}\n\t\tlet result = [];\n\t\tthis.forEach((value, key) => result.push(fn(value, key)));\n\t\tif (!raw) {\n\t\t\tresult = result.map(i => this.list(i));\n\t\t\tif (this.immutable) {\n\t\t\t\tresult = Object.freeze(result);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Merges two values together with support for arrays and objects\n\t * @param {*} a - First value (target)\n\t * @param {*} b - Second value (source)\n\t * @param {boolean} [override=false] - Whether to override arrays instead of concatenating\n\t * @returns {*} Merged result\n\t * @example\n\t * const merged = store.merge({a: 1}, {b: 2}); // {a: 1, b: 2}\n\t * const arrays = store.merge([1, 2], [3, 4]); // [1, 2, 3, 4]\n\t */\n\tmerge (a, b, override = false) {\n\t\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\t\ta = override ? b : a.concat(b);\n\t\t} else if (typeof a === STRING_OBJECT && a !== null && typeof b === STRING_OBJECT && b !== null) {\n\t\t\tthis.each(Object.keys(b), i => {\n\t\t\t\ta[i] = this.merge(a[i], b[i], override);\n\t\t\t});\n\t\t} else {\n\t\t\ta = b;\n\t\t}\n\n\t\treturn a;\n\t}\n\n\t/**\n\t * Lifecycle hook executed after batch operations for custom postprocessing\n\t * @param {Array} arg - Result of batch operation\n\t * @param {string} [type=STRING_EMPTY] - Type of batch operation that was performed\n\t * @returns {Array} Modified result (override this method to implement custom logic)\n\t */\n\tonbatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\treturn arg;\n\t}\n\n\t/**\n\t * Lifecycle hook executed after clear operation for custom postprocessing\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t * @example\n\t * class MyStore extends Haro {\n\t * onclear() {\n\t * console.log('Store cleared');\n\t * }\n\t * }\n\t */\n\tonclear () {\n\t\t// Hook for custom logic after clear; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed after delete operation for custom postprocessing\n\t * @param {string} [key=STRING_EMPTY] - Key of deleted record\n\t * @param {boolean} [batch=false] - Whether this was part of a batch operation\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tondelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic after delete; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed after override operation for custom postprocessing\n\t * @param {string} [type=STRING_EMPTY] - Type of override operation that was performed\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tonoverride (type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic after override; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed after set operation for custom postprocessing\n\t * @param {Object} [arg={}] - Record that was set\n\t * @param {boolean} [batch=false] - Whether this was part of a batch operation\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tonset (arg = {}, batch = false) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic after set; override in subclass if needed\n\t}\n\n\t/**\n\t * Replaces all store data or indexes with new data for bulk operations\n\t * @param {Array} data - Data to replace with (format depends on type)\n\t * @param {string} [type=STRING_RECORDS] - Type of data: 'records' or 'indexes'\n\t * @returns {boolean} True if operation succeeded\n\t * @throws {Error} Throws error if type is invalid\n\t * @example\n\t * const records = [['key1', {name: 'John'}], ['key2', {name: 'Jane'}]];\n\t * store.override(records, 'records');\n\t */\n\toverride (data, type = STRING_RECORDS) {\n\t\tconst result = true;\n\t\tif (type === STRING_INDEXES) {\n\t\t\tthis.indexes = new Map(data.map(i => [i[0], new Map(i[1].map(ii => [ii[0], new Set(ii[1])]))]));\n\t\t} else if (type === STRING_RECORDS) {\n\t\t\tthis.indexes.clear();\n\t\t\tthis.data = new Map(data);\n\t\t} else {\n\t\t\tthrow new Error(STRING_INVALID_TYPE);\n\t\t}\n\t\tthis.onoverride(type);\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Reduces all records to a single value using a reducer function\n\t * @param {Function} fn - Reducer function (accumulator, value, key, store)\n\t * @param {*} [accumulator] - Initial accumulator value\n\t * @returns {*} Final reduced value\n\t * @example\n\t * const totalAge = store.reduce((sum, record) => sum + record.age, 0);\n\t * const names = store.reduce((acc, record) => acc.concat(record.name), []);\n\t */\n\treduce (fn, accumulator = []) {\n\t\tlet a = accumulator;\n\t\tthis.forEach((v, k) => {\n\t\t\ta = fn(a, v, k, this);\n\t\t}, this);\n\n\t\treturn a;\n\t}\n\n\t/**\n\t * Rebuilds indexes for specified fields or all fields for data consistency\n\t * @param {string|string[]} [index] - Specific index field(s) to rebuild, or all if not specified\n\t * @returns {Haro} This instance for method chaining\n\t * @example\n\t * store.reindex(); // Rebuild all indexes\n\t * store.reindex('name'); // Rebuild only name index\n\t * store.reindex(['name', 'email']); // Rebuild name and email indexes\n\t */\n\treindex (index) {\n\t\tconst indices = index ? [index] : this.index;\n\t\tif (index && this.index.includes(index) === false) {\n\t\t\tthis.index.push(index);\n\t\t}\n\t\tthis.each(indices, i => this.indexes.set(i, new Map()));\n\t\tthis.forEach((data, key) => this.each(indices, i => this.setIndex(key, data, i)));\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Searches for records containing a value across specified indexes\n\t * @param {*} value - Value to search for (string, function, or RegExp)\n\t * @param {string|string[]} [index] - Index(es) to search in, or all if not specified\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of matching records\n\t * @example\n\t * const results = store.search('john'); // Search all indexes\n\t * const nameResults = store.search('john', 'name'); // Search only name index\n\t * const regexResults = store.search(/^admin/, 'role'); // Regex search\n\t */\n\tsearch (value, index, raw = false) {\n\t\tconst result = new Set(); // Use Set for unique keys\n\t\tconst fn = typeof value === STRING_FUNCTION;\n\t\tconst rgex = value && typeof value.test === STRING_FUNCTION;\n\t\tif (!value) return this.immutable ? this.freeze() : [];\n\t\tconst indices = index ? Array.isArray(index) ? index : [index] : this.index;\n\t\tfor (const i of indices) {\n\t\t\tconst idx = this.indexes.get(i);\n\t\t\tif (idx) {\n\t\t\t\tfor (const [lkey, lset] of idx) {\n\t\t\t\t\tlet match = false;\n\n\t\t\t\t\tif (fn) {\n\t\t\t\t\t\tmatch = value(lkey, i);\n\t\t\t\t\t} else if (rgex) {\n\t\t\t\t\t\tmatch = value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmatch = lkey === value;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (match) {\n\t\t\t\t\t\tfor (const key of lset) {\n\t\t\t\t\t\t\tif (this.data.has(key)) {\n\t\t\t\t\t\t\t\tresult.add(key);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlet records = Array.from(result).map(key => this.get(key, raw));\n\t\tif (!raw && this.immutable) {\n\t\t\trecords = Object.freeze(records);\n\t\t}\n\n\t\treturn records;\n\t}\n\n\t/**\n\t * Sets or updates a record in the store with automatic indexing\n\t * @param {string|null} [key=null] - Key for the record, or null to use record's key field\n\t * @param {Object} [data={}] - Record data to set\n\t * @param {boolean} [batch=false] - Whether this is part of a batch operation\n\t * @param {boolean} [override=false] - Whether to override existing data instead of merging\n\t * @returns {Object} The stored record (frozen if immutable mode)\n\t * @example\n\t * const user = store.set(null, {name: 'John', age: 30}); // Auto-generate key\n\t * const updated = store.set('user123', {age: 31}); // Update existing record\n\t */\n\tset (key = null, data = {}, batch = false, override = false) {\n\t\tif (key === null) {\n\t\t\tkey = data[this.key] ?? this.uuid();\n\t\t}\n\t\tlet x = {...data, [this.key]: key};\n\t\tthis.beforeSet(key, x, batch, override);\n\t\tif (!this.data.has(key)) {\n\t\t\tif (this.versioning) {\n\t\t\t\tthis.versions.set(key, new Set());\n\t\t\t}\n\t\t} else {\n\t\t\tconst og = this.get(key, true);\n\t\t\tthis.deleteIndex(key, og);\n\t\t\tif (this.versioning) {\n\t\t\t\tthis.versions.get(key).add(Object.freeze(this.clone(og)));\n\t\t\t}\n\t\t\tif (!override) {\n\t\t\t\tx = this.merge(this.clone(og), x);\n\t\t\t}\n\t\t}\n\t\tthis.data.set(key, x);\n\t\tthis.setIndex(key, x, null);\n\t\tconst result = this.get(key);\n\t\tthis.onset(result, batch);\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Internal method to add entries to indexes for a record\n\t * @param {string} key - Key of record being indexed\n\t * @param {Object} data - Data of record being indexed\n\t * @param {string|null} indice - Specific index to update, or null for all\n\t * @returns {Haro} This instance for method chaining\n\t */\n\tsetIndex (key, data, indice) {\n\t\tthis.each(indice === null ? this.index : [indice], i => {\n\t\t\tlet idx = this.indexes.get(i);\n\t\t\tif (!idx) {\n\t\t\t\tidx = new Map();\n\t\t\t\tthis.indexes.set(i, idx);\n\t\t\t}\n\t\t\tconst fn = c => {\n\t\t\t\tif (!idx.has(c)) {\n\t\t\t\t\tidx.set(c, new Set());\n\t\t\t\t}\n\t\t\t\tidx.get(c).add(key);\n\t\t\t};\n\t\t\tif (i.includes(this.delimiter)) {\n\t\t\t\tthis.each(this.indexKeys(i, this.delimiter, data), fn);\n\t\t\t} else {\n\t\t\t\tthis.each(Array.isArray(data[i]) ? data[i] : [data[i]], fn);\n\t\t\t}\n\t\t});\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Sorts all records using a comparator function\n\t * @param {Function} fn - Comparator function for sorting (a, b) => number\n\t * @param {boolean} [frozen=false] - Whether to return frozen records\n\t * @returns {Array} Sorted array of records\n\t * @example\n\t * const sorted = store.sort((a, b) => a.age - b.age); // Sort by age\n\t * const names = store.sort((a, b) => a.name.localeCompare(b.name)); // Sort by name\n\t */\n\tsort (fn, frozen = false) {\n\t\tconst dataSize = this.data.size;\n\t\tlet result = this.limit(INT_0, dataSize, true).sort(fn);\n\t\tif (frozen) {\n\t\t\tresult = this.freeze(...result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Comparator function for sorting keys with type-aware comparison logic\n\t * @param {*} a - First value to compare\n\t * @param {*} b - Second value to compare\n\t * @returns {number} Negative number if a < b, positive if a > b, zero if equal\n\t * @example\n\t * const keys = ['name', 'age', 'email'];\n\t * keys.sort(store.sortKeys); // Alphabetical sort\n\t *\n\t * const mixed = [10, '5', 'abc', 3];\n\t * mixed.sort(store.sortKeys); // Type-aware sort: numbers first, then strings\n\t */\n\tsortKeys (a, b) {\n\t\t// Handle string comparison\n\t\tif (typeof a === STRING_STRING && typeof b === STRING_STRING) {\n\t\t\treturn a.localeCompare(b);\n\t\t}\n\t\t// Handle numeric comparison\n\t\tif (typeof a === STRING_NUMBER && typeof b === STRING_NUMBER) {\n\t\t\treturn a - b;\n\t\t}\n\n\t\t// Handle mixed types or other types by converting to string\n\n\t\treturn String(a).localeCompare(String(b));\n\t}\n\n\t/**\n\t * Sorts records by a specific indexed field in ascending order\n\t * @param {string} [index=STRING_EMPTY] - Index field name to sort by\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of records sorted by the specified field\n\t * @throws {Error} Throws error if index field is empty or invalid\n\t * @example\n\t * const byAge = store.sortBy('age');\n\t * const byName = store.sortBy('name');\n\t */\n\tsortBy (index = STRING_EMPTY, raw = false) {\n\t\tif (index === STRING_EMPTY) {\n\t\t\tthrow new Error(STRING_INVALID_FIELD);\n\t\t}\n\t\tlet result = [];\n\t\tconst keys = [];\n\t\tif (this.indexes.has(index) === false) {\n\t\t\tthis.reindex(index);\n\t\t}\n\t\tconst lindex = this.indexes.get(index);\n\t\tlindex.forEach((idx, key) => keys.push(key));\n\t\tthis.each(keys.sort(this.sortKeys), i => lindex.get(i).forEach(key => result.push(this.get(key, raw))));\n\t\tif (this.immutable) {\n\t\t\tresult = Object.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Converts all store data to a plain array of records\n\t * @returns {Array} Array containing all records in the store\n\t * @example\n\t * const allRecords = store.toArray();\n\t * console.log(`Store contains ${allRecords.length} records`);\n\t */\n\ttoArray () {\n\t\tconst result = Array.from(this.data.values());\n\t\tif (this.immutable) {\n\t\t\tthis.each(result, i => Object.freeze(i));\n\t\t\tObject.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Generates a RFC4122 v4 UUID for record identification\n\t * @returns {string} UUID string in standard format\n\t * @example\n\t * const id = store.uuid(); // \"f47ac10b-58cc-4372-a567-0e02b2c3d479\"\n\t */\n\tuuid () {\n\t\treturn uuid();\n\t}\n\n\t/**\n\t * Returns an iterator of all values in the store\n\t * @returns {Iterator} Iterator of record values\n\t * @example\n\t * for (const record of store.values()) {\n\t * console.log(record.name);\n\t * }\n\t */\n\tvalues () {\n\t\treturn this.data.values();\n\t}\n\n\t/**\n\t * Internal helper method for predicate matching with support for arrays and regex\n\t * @param {Object} record - Record to test against predicate\n\t * @param {Object} predicate - Predicate object with field-value pairs\n\t * @param {string} op - Operator for array matching ('||' for OR, '&&' for AND)\n\t * @returns {boolean} True if record matches predicate criteria\n\t */\n\tmatchesPredicate (record, predicate, op) {\n\t\tconst keys = Object.keys(predicate);\n\n\t\treturn keys.every(key => {\n\t\t\tconst pred = predicate[key];\n\t\t\tconst val = record[key];\n\t\t\tif (Array.isArray(pred)) {\n\t\t\t\tif (Array.isArray(val)) {\n\t\t\t\t\treturn op === STRING_DOUBLE_AND ? pred.every(p => val.includes(p)) : pred.some(p => val.includes(p));\n\t\t\t\t} else {\n\t\t\t\t\treturn op === STRING_DOUBLE_AND ? pred.every(p => val === p) : pred.some(p => val === p);\n\t\t\t\t}\n\t\t\t} else if (pred instanceof RegExp) {\n\t\t\t\tif (Array.isArray(val)) {\n\t\t\t\t\treturn op === STRING_DOUBLE_AND ? val.every(v => pred.test(v)) : val.some(v => pred.test(v));\n\t\t\t\t} else {\n\t\t\t\t\treturn pred.test(val);\n\t\t\t\t}\n\t\t\t} else if (Array.isArray(val)) {\n\t\t\t\treturn val.includes(pred);\n\t\t\t} else {\n\t\t\t\treturn val === pred;\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Advanced filtering with predicate logic supporting AND/OR operations on arrays\n\t * @param {Object} [predicate={}] - Object with field-value pairs for filtering\n\t * @param {string} [op=STRING_DOUBLE_PIPE] - Operator for array matching ('||' for OR, '&&' for AND)\n\t * @returns {Array} Array of records matching the predicate criteria\n\t * @example\n\t * // Find records with tags containing 'admin' OR 'user'\n\t * const users = store.where({tags: ['admin', 'user']}, '||');\n\t *\n\t * // Find records with ALL specified tags\n\t * const powerUsers = store.where({tags: ['admin', 'power']}, '&&');\n\t *\n\t * // Regex matching\n\t * const emails = store.where({email: /^admin@/});\n\t */\n\twhere (predicate = {}, op = STRING_DOUBLE_PIPE) {\n\t\tconst keys = this.index.filter(i => i in predicate);\n\t\tif (keys.length === 0) return [];\n\n\t\t// Try to use indexes for better performance\n\t\tconst indexedKeys = keys.filter(k => this.indexes.has(k));\n\t\tif (indexedKeys.length > 0) {\n\t\t\t// Use index-based filtering for better performance\n\t\t\tlet candidateKeys = new Set();\n\t\t\tlet first = true;\n\t\t\tfor (const key of indexedKeys) {\n\t\t\t\tconst pred = predicate[key];\n\t\t\t\tconst idx = this.indexes.get(key);\n\t\t\t\tconst matchingKeys = new Set();\n\t\t\t\tif (Array.isArray(pred)) {\n\t\t\t\t\tfor (const p of pred) {\n\t\t\t\t\t\tif (idx.has(p)) {\n\t\t\t\t\t\t\tfor (const k of idx.get(p)) {\n\t\t\t\t\t\t\t\tmatchingKeys.add(k);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (idx.has(pred)) {\n\t\t\t\t\tfor (const k of idx.get(pred)) {\n\t\t\t\t\t\tmatchingKeys.add(k);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (first) {\n\t\t\t\t\tcandidateKeys = matchingKeys;\n\t\t\t\t\tfirst = false;\n\t\t\t\t} else {\n\t\t\t\t\t// AND operation across different fields\n\t\t\t\t\tcandidateKeys = new Set([...candidateKeys].filter(k => matchingKeys.has(k)));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Filter candidates with full predicate logic\n\t\t\tconst results = [];\n\t\t\tfor (const key of candidateKeys) {\n\t\t\t\tconst record = this.get(key, true);\n\t\t\t\tif (this.matchesPredicate(record, predicate, op)) {\n\t\t\t\t\tresults.push(this.immutable ? this.get(key) : record);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn this.immutable ? this.freeze(...results) : results;\n\t\t}\n\n\t\t// Fallback to full scan if no indexes available\n\t\treturn this.filter(a => this.matchesPredicate(a, predicate, op));\n\t}\n}\n\n/**\n * Factory function to create a new Haro instance with optional initial data\n * @param {Array|null} [data=null] - Initial data to populate the store\n * @param {Object} [config={}] - Configuration object passed to Haro constructor\n * @returns {Haro} New Haro instance configured and optionally populated\n * @example\n * const store = haro([\n * {id: 1, name: 'John', age: 30},\n * {id: 2, name: 'Jane', age: 25}\n * ], {\n * index: ['name', 'age'],\n * versioning: true\n * });\n */\nexport function haro (data = null, config = {}) {\n\tconst obj = new Haro(config);\n\n\tif (Array.isArray(data)) {\n\t\tobj.batch(data, STRING_SET);\n\t}\n\n\treturn obj;\n}\n"],"names":["randomUUID","STRING_EMPTY","STRING_DOUBLE_AND","STRING_FUNCTION","STRING_OBJECT","STRING_RECORDS","STRING_STRING","STRING_NUMBER","STRING_INVALID_FUNCTION","Haro","constructor","delimiter","id","this","uuid","immutable","index","key","versioning","data","Map","Array","isArray","indexes","versions","Object","defineProperty","enumerable","get","from","keys","size","reindex","batch","args","type","fn","i","delete","set","onbatch","beforeBatch","map","arg","beforeClear","beforeDelete","beforeSet","override","clear","onclear","clone","structuredClone","has","Error","og","deleteIndex","ondelete","forEach","idx","values","includes","indexKeys","each","value","o","dump","result","entries","ii","arr","len","length","find","where","raw","sort","sortKeys","join","reduce","a","v","k","add","Set","freeze","filter","push","list","ctx","call","fields","split","fieldsLen","field","newResult","resultLen","valuesLen","j","newKey","limit","offset","max","registry","slice","merge","b","concat","onoverride","onset","accumulator","indices","setIndex","search","rgex","test","lkey","lset","match","records","x","indice","c","frozen","dataSize","localeCompare","String","sortBy","lindex","toArray","matchesPredicate","record","predicate","op","every","pred","val","p","some","RegExp","indexedKeys","candidateKeys","first","matchingKeys","results","haro","config","obj"],"mappings":"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"} \ No newline at end of file diff --git a/dist/haro.umd.js b/dist/haro.umd.js index 8d44520a..7f24f2bf 100644 --- a/dist/haro.umd.js +++ b/dist/haro.umd.js @@ -3,56 +3,79 @@ * * @copyright 2025 Jason Mulligan * @license BSD-3-Clause - * @version 15.2.6 + * @version 16.0.0 */ -(function(g,f){typeof exports==='object'&&typeof module!=='undefined'?f(exports):typeof define==='function'&&define.amd?define(['exports'],f):(g=typeof globalThis!=='undefined'?globalThis:g||self,f(g.lru={}));})(this,(function(exports){'use strict';const STRING_COMMA = ","; +(function(g,f){typeof exports==='object'&&typeof module!=='undefined'?f(exports,require('crypto')):typeof define==='function'&&define.amd?define(['exports','crypto'],f):(g=typeof globalThis!=='undefined'?globalThis:g||self,f(g.lru={},g.crypto));})(this,(function(exports,crypto){'use strict';// String constants - Single characters and symbols +const STRING_COMMA = ","; const STRING_EMPTY = ""; const STRING_PIPE = "|"; const STRING_DOUBLE_PIPE = "||"; -const STRING_A = "a"; -const STRING_B = "b"; +const STRING_DOUBLE_AND = "&&"; + +// String constants - Operation and type names +const STRING_ID = "id"; const STRING_DEL = "del"; const STRING_FUNCTION = "function"; const STRING_INDEXES = "indexes"; -const STRING_INVALID_FIELD = "Invalid field"; -const STRING_INVALID_FUNCTION = "Invalid function"; -const STRING_INVALID_TYPE = "Invalid type"; const STRING_OBJECT = "object"; -const STRING_RECORD_NOT_FOUND = "Record not found"; const STRING_RECORDS = "records"; const STRING_REGISTRY = "registry"; const STRING_SET = "set"; const STRING_SIZE = "size"; -const INT_0 = 0; -const INT_1 = 1; -const INT_3 = 3; -const INT_4 = 4; -const INT_8 = 8; -const INT_9 = 9; -const INT_16 = 16;/* istanbul ignore next */ -const r = [INT_8, INT_9, STRING_A, STRING_B]; - -/* istanbul ignore next */ -function s () { - return ((Math.random() + INT_1) * 0x10000 | INT_0).toString(INT_16).substring(INT_1); -} +const STRING_STRING = "string"; +const STRING_NUMBER = "number"; -/* istanbul ignore next */ -function randomUUID () { - return `${s()}${s()}-${s()}-4${s().slice(INT_0, INT_3)}-${r[Math.floor(Math.random() * INT_4)]}${s().slice(INT_0, INT_3)}-${s()}${s()}${s()}`; -} +// String constants - Error messages +const STRING_INVALID_FIELD = "Invalid field"; +const STRING_INVALID_FUNCTION = "Invalid function"; +const STRING_INVALID_TYPE = "Invalid type"; +const STRING_RECORD_NOT_FOUND = "Record not found"; -const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : randomUUID;class Haro { - constructor ({delimiter = STRING_PIPE, id = this.uuid(), index = [], key = "id", versioning = false} = {}) { +// Integer constants +const INT_0 = 0;/** + * Haro is a modern immutable DataStore for collections of records with indexing, + * versioning, and batch operations support. It provides a Map-like interface + * with advanced querying capabilities through indexes. + * @class + * @example + * const store = new Haro({ + * index: ['name', 'age'], + * key: 'id', + * versioning: true + * }); + * + * store.set(null, {name: 'John', age: 30}); + * const results = store.find({name: 'John'}); + */ +class Haro { + /** + * Creates a new Haro instance with specified configuration + * @param {Object} [config={}] - Configuration object for the store + * @param {string} [config.delimiter=STRING_PIPE] - Delimiter for composite indexes (default: '|') + * @param {string} [config.id] - Unique identifier for this instance (auto-generated if not provided) + * @param {boolean} [config.immutable=false] - Return frozen/immutable objects for data safety + * @param {string[]} [config.index=[]] - Array of field names to create indexes for + * @param {string} [config.key=STRING_ID] - Primary key field name used for record identification + * @param {boolean} [config.versioning=false] - Enable versioning to track record changes + * @constructor + * @example + * const store = new Haro({ + * index: ['name', 'email', 'name|department'], + * key: 'userId', + * versioning: true, + * immutable: true + * }); + */ + constructor ({delimiter = STRING_PIPE, id = this.uuid(), immutable = false, index = [], key = STRING_ID, versioning = false} = {}) { this.data = new Map(); this.delimiter = delimiter; this.id = id; + this.immutable = immutable; this.index = Array.isArray(index) ? [...index] : []; this.indexes = new Map(); this.key = key; this.versions = new Map(); this.versioning = versioning; - Object.defineProperty(this, STRING_REGISTRY, { enumerable: true, get: () => Array.from(this.data.keys()) @@ -65,28 +88,78 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return this.reindex(); } + /** + * Performs batch operations on multiple records for efficient bulk processing + * @param {Array} args - Array of records to process + * @param {string} [type=STRING_SET] - Type of operation: 'set' for upsert, 'del' for delete + * @returns {Array} Array of results from the batch operation + * @throws {Error} Throws error if individual operations fail during batch processing + * @example + * const results = store.batch([ + * {id: 1, name: 'John'}, + * {id: 2, name: 'Jane'} + * ], 'set'); + */ batch (args, type = STRING_SET) { - const fn = type === STRING_DEL ? i => this.del(i, true) : i => this.set(null, i, true, true); + const fn = type === STRING_DEL ? i => this.delete(i, true) : i => this.set(null, i, true, true); return this.onbatch(this.beforeBatch(args, type).map(fn), type); } + /** + * Lifecycle hook executed before batch operations for custom preprocessing + * @param {Array} arg - Arguments passed to batch operation + * @param {string} [type=STRING_EMPTY] - Type of batch operation ('set' or 'del') + * @returns {Array} The arguments array (possibly modified) to be processed + */ beforeBatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars + // Hook for custom logic before batch; override in subclass if needed return arg; } + /** + * Lifecycle hook executed before clear operation for custom preprocessing + * @returns {void} Override this method in subclasses to implement custom logic + * @example + * class MyStore extends Haro { + * beforeClear() { + * this.backup = this.toArray(); + * } + * } + */ beforeClear () { // Hook for custom logic before clear; override in subclass if needed } - beforeDelete (key = STRING_EMPTY, batch = false) { - return [key, batch]; - } - - beforeSet (key = STRING_EMPTY, batch = false) { - return [key, batch]; - } - + /** + * Lifecycle hook executed before delete operation for custom preprocessing + * @param {string} [key=STRING_EMPTY] - Key of record to delete + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + beforeDelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic before delete; override in subclass if needed + } + + /** + * Lifecycle hook executed before set operation for custom preprocessing + * @param {string} [key=STRING_EMPTY] - Key of record to set + * @param {Object} [data={}] - Record data being set + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @param {boolean} [override=false] - Whether to override existing data + * @returns {void} Override this method in subclasses to implement custom logic + */ + beforeSet (key = STRING_EMPTY, data = {}, batch = false, override = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic before set; override in subclass if needed + } + + /** + * Removes all records, indexes, and versions from the store + * @returns {Haro} This instance for method chaining + * @example + * store.clear(); + * console.log(store.size); // 0 + */ clear () { this.beforeClear(); this.data.clear(); @@ -97,17 +170,36 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return this; } + /** + * Creates a deep clone of the given value, handling objects, arrays, and primitives + * @param {*} arg - Value to clone (any type) + * @returns {*} Deep clone of the argument + * @example + * const original = {name: 'John', tags: ['user', 'admin']}; + * const cloned = store.clone(original); + * cloned.tags.push('new'); // original.tags is unchanged + */ clone (arg) { - return JSON.parse(JSON.stringify(arg)); - } - - del (key = STRING_EMPTY, batch = false) { + return structuredClone(arg); + } + + /** + * Deletes a record from the store and removes it from all indexes + * @param {string} [key=STRING_EMPTY] - Key of record to delete + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @returns {void} + * @throws {Error} Throws error if record with the specified key is not found + * @example + * store.delete('user123'); + * // Throws error if 'user123' doesn't exist + */ + delete (key = STRING_EMPTY, batch = false) { if (!this.data.has(key)) { throw new Error(STRING_RECORD_NOT_FOUND); } const og = this.get(key, true); this.beforeDelete(key, batch); - this.delIndex(this.index, this.indexes, this.delimiter, key, og); + this.deleteIndex(key, og); this.data.delete(key); this.ondelete(key, batch); if (this.versioning) { @@ -115,12 +207,18 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } } - delIndex (index, indexes, delimiter, key, data) { - index.forEach(i => { - const idx = indexes.get(i); + /** + * Internal method to remove entries from indexes for a deleted record + * @param {string} key - Key of record being deleted + * @param {Object} data - Data of record being deleted + * @returns {Haro} This instance for method chaining + */ + deleteIndex (key, data) { + this.index.forEach(i => { + const idx = this.indexes.get(i); if (!idx) return; - const values = i.includes(delimiter) ? - this.indexKeys(i, delimiter, data) : + const values = i.includes(this.delimiter) ? + this.indexKeys(i, this.delimiter, data) : Array.isArray(data[i]) ? data[i] : [data[i]]; this.each(values, value => { if (idx.has(value)) { @@ -132,11 +230,20 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } }); }); + + return this; } + /** + * Exports complete store data or indexes for persistence or debugging + * @param {string} [type=STRING_RECORDS] - Type of data to export: 'records' or 'indexes' + * @returns {Array} Array of [key, value] pairs for records, or serialized index structure + * @example + * const records = store.dump('records'); + * const indexes = store.dump('indexes'); + */ dump (type = STRING_RECORDS) { let result; - if (type === STRING_RECORDS) { result = Array.from(this.entries()); } else { @@ -154,20 +261,46 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return result; } + /** + * Utility method to iterate over an array with a callback function + * @param {Array<*>} [arr=[]] - Array to iterate over + * @param {Function} fn - Function to call for each element (element, index) + * @returns {Array<*>} The original array for method chaining + * @example + * store.each([1, 2, 3], (item, index) => console.log(item, index)); + */ each (arr = [], fn) { - for (const [idx, value] of arr.entries()) { - fn(value, idx); + const len = arr.length; + for (let i = 0; i < len; i++) { + fn(arr[i], i); } return arr; } + /** + * Returns an iterator of [key, value] pairs for each record in the store + * @returns {Iterator>} Iterator of [key, value] pairs + * @example + * for (const [key, value] of store.entries()) { + * console.log(key, value); + * } + */ entries () { return this.data.entries(); } + /** + * Finds records matching the specified criteria using indexes for optimal performance + * @param {Object} [where={}] - Object with field-value pairs to match against + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of matching records (frozen if immutable mode) + * @example + * const users = store.find({department: 'engineering', active: true}); + * const admins = store.find({role: 'admin'}); + */ find (where = {}, raw = false) { - const key = Object.keys(where).sort((a, b) => a.localeCompare(b)).join(this.delimiter); + const key = Object.keys(where).sort(this.sortKeys).join(this.delimiter); const index = this.indexes.get(key) ?? new Map(); let result = []; if (index.size > 0) { @@ -180,82 +313,230 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return a; }, new Set())).map(i => this.get(i, raw)); } + if (!raw && this.immutable) { + result = Object.freeze(result); + } - return raw ? result : this.list(...result); + return result; } + /** + * Filters records using a predicate function, similar to Array.filter + * @param {Function} fn - Predicate function to test each record (record, key, store) + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records that pass the predicate test + * @throws {Error} Throws error if fn is not a function + * @example + * const adults = store.filter(record => record.age >= 18); + * const recent = store.filter(record => record.created > Date.now() - 86400000); + */ filter (fn, raw = false) { if (typeof fn !== STRING_FUNCTION) { throw new Error(STRING_INVALID_FUNCTION); } - const x = raw ? (k, v) => v : (k, v) => Object.freeze([k, Object.freeze(v)]); - const result = this.reduce((a, v, k, ctx) => { - if (fn.call(ctx, v)) { - a.push(x(k, v)); + let result = this.reduce((a, v) => { + if (fn(v)) { + a.push(v); } return a; }, []); + if (!raw) { + result = result.map(i => this.list(i)); + + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : Object.freeze(result); + return result; } - forEach (fn, ctx) { - this.data.forEach((value, key) => fn(this.clone(value), this.clone(key)), ctx ?? this.data); + /** + * Executes a function for each record in the store, similar to Array.forEach + * @param {Function} fn - Function to execute for each record (value, key) + * @param {*} [ctx] - Context object to use as 'this' when executing the function + * @returns {Haro} This instance for method chaining + * @example + * store.forEach((record, key) => { + * console.log(`${key}: ${record.name}`); + * }); + */ + forEach (fn, ctx = this) { + this.data.forEach((value, key) => { + if (this.immutable) { + value = this.clone(value); + } + fn.call(ctx, value, key); + }, this); return this; } + /** + * Creates a frozen array from the given arguments for immutable data handling + * @param {...*} args - Arguments to freeze into an array + * @returns {Array<*>} Frozen array containing frozen arguments + * @example + * const frozen = store.freeze(obj1, obj2, obj3); + * // Returns Object.freeze([Object.freeze(obj1), Object.freeze(obj2), Object.freeze(obj3)]) + */ + freeze (...args) { + return Object.freeze(args.map(i => Object.freeze(i))); + } + + /** + * Retrieves a record by its key + * @param {string} key - Key of record to retrieve + * @param {boolean} [raw=false] - Whether to return raw data (true) or processed/frozen data (false) + * @returns {Object|null} The record if found, null if not found + * @example + * const user = store.get('user123'); + * const rawUser = store.get('user123', true); + */ get (key, raw = false) { - const result = this.clone(this.data.get(key) ?? null); + let result = this.data.get(key) ?? null; + if (result !== null && !raw) { + result = this.list(result); + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : this.list(key, result); + return result; } + /** + * Checks if a record with the specified key exists in the store + * @param {string} key - Key to check for existence + * @returns {boolean} True if record exists, false otherwise + * @example + * if (store.has('user123')) { + * console.log('User exists'); + * } + */ has (key) { return this.data.has(key); } + /** + * Generates index keys for composite indexes from data values + * @param {string} [arg=STRING_EMPTY] - Composite index field names joined by delimiter + * @param {string} [delimiter=STRING_PIPE] - Delimiter used in composite index + * @param {Object} [data={}] - Data object to extract field values from + * @returns {string[]} Array of generated index keys + * @example + * // For index 'name|department' with data {name: 'John', department: 'IT'} + * const keys = store.indexKeys('name|department', '|', data); + * // Returns ['John|IT'] + */ indexKeys (arg = STRING_EMPTY, delimiter = STRING_PIPE, data = {}) { - return arg.split(delimiter).reduce((a, li, lidx) => { - const result = []; - - (Array.isArray(data[li]) ? data[li] : [data[li]]).forEach(lli => lidx === INT_0 ? result.push(lli) : a.forEach(x => result.push(`${x}${delimiter}${lli}`))); + const fields = arg.split(delimiter).sort(this.sortKeys); + const fieldsLen = fields.length; + let result = [""]; + for (let i = 0; i < fieldsLen; i++) { + const field = fields[i]; + const values = Array.isArray(data[field]) ? data[field] : [data[field]]; + const newResult = []; + const resultLen = result.length; + const valuesLen = values.length; + for (let j = 0; j < resultLen; j++) { + for (let k = 0; k < valuesLen; k++) { + const newKey = i === 0 ? values[k] : `${result[j]}${delimiter}${values[k]}`; + newResult.push(newKey); + } + } + result = newResult; + } - return result; - }, []); + return result; } + /** + * Returns an iterator of all keys in the store + * @returns {Iterator} Iterator of record keys + * @example + * for (const key of store.keys()) { + * console.log(key); + * } + */ keys () { return this.data.keys(); } + /** + * Returns a limited subset of records with offset support for pagination + * @param {number} [offset=INT_0] - Number of records to skip from the beginning + * @param {number} [max=INT_0] - Maximum number of records to return + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records within the specified range + * @example + * const page1 = store.limit(0, 10); // First 10 records + * const page2 = store.limit(10, 10); // Next 10 records + */ limit (offset = INT_0, max = INT_0, raw = false) { - const result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw)); - - return raw ? result : this.list(...result); - } + let result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw)); + if (!raw && this.immutable) { + result = Object.freeze(result); + } - list (...args) { - return Object.freeze(args.map(i => Object.freeze(i))); + return result; } + /** + * Converts a record into a [key, value] pair array format + * @param {Object} arg - Record object to convert to list format + * @returns {Array<*>} Array containing [key, record] where key is extracted from record's key field + * @example + * const record = {id: 'user123', name: 'John', age: 30}; + * const pair = store.list(record); // ['user123', {id: 'user123', name: 'John', age: 30}] + */ + list (arg) { + const result = [arg[this.key], arg]; + + return this.immutable ? this.freeze(...result) : result; + } + + /** + * Transforms all records using a mapping function, similar to Array.map + * @param {Function} fn - Function to transform each record (record, key) + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array<*>} Array of transformed results + * @throws {Error} Throws error if fn is not a function + * @example + * const names = store.map(record => record.name); + * const summaries = store.map(record => ({id: record.id, name: record.name})); + */ map (fn, raw = false) { if (typeof fn !== STRING_FUNCTION) { throw new Error(STRING_INVALID_FUNCTION); } - - const result = []; - + let result = []; this.forEach((value, key) => result.push(fn(value, key))); + if (!raw) { + result = result.map(i => this.list(i)); + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : this.list(...result); + return result; } + /** + * Merges two values together with support for arrays and objects + * @param {*} a - First value (target) + * @param {*} b - Second value (source) + * @param {boolean} [override=false] - Whether to override arrays instead of concatenating + * @returns {*} Merged result + * @example + * const merged = store.merge({a: 1}, {b: 2}); // {a: 1, b: 2} + * const arrays = store.merge([1, 2], [3, 4]); // [1, 2, 3, 4] + */ merge (a, b, override = false) { if (Array.isArray(a) && Array.isArray(b)) { a = override ? b : a.concat(b); - } else if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) { + } else if (typeof a === STRING_OBJECT && a !== null && typeof b === STRING_OBJECT && b !== null) { this.each(Object.keys(b), i => { a[i] = this.merge(a[i], b[i], override); }); @@ -266,29 +547,71 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return a; } + /** + * Lifecycle hook executed after batch operations for custom postprocessing + * @param {Array} arg - Result of batch operation + * @param {string} [type=STRING_EMPTY] - Type of batch operation that was performed + * @returns {Array} Modified result (override this method to implement custom logic) + */ onbatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars return arg; } + /** + * Lifecycle hook executed after clear operation for custom postprocessing + * @returns {void} Override this method in subclasses to implement custom logic + * @example + * class MyStore extends Haro { + * onclear() { + * console.log('Store cleared'); + * } + * } + */ onclear () { // Hook for custom logic after clear; override in subclass if needed } - ondelete (key = STRING_EMPTY, batch = false) { - return [key, batch]; - } - - onoverride (type = STRING_EMPTY) { - return type; - } - - onset (arg = {}, batch = false) { - return [arg, batch]; - } - + /** + * Lifecycle hook executed after delete operation for custom postprocessing + * @param {string} [key=STRING_EMPTY] - Key of deleted record + * @param {boolean} [batch=false] - Whether this was part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + ondelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic after delete; override in subclass if needed + } + + /** + * Lifecycle hook executed after override operation for custom postprocessing + * @param {string} [type=STRING_EMPTY] - Type of override operation that was performed + * @returns {void} Override this method in subclasses to implement custom logic + */ + onoverride (type = STRING_EMPTY) { // eslint-disable-line no-unused-vars + // Hook for custom logic after override; override in subclass if needed + } + + /** + * Lifecycle hook executed after set operation for custom postprocessing + * @param {Object} [arg={}] - Record that was set + * @param {boolean} [batch=false] - Whether this was part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + onset (arg = {}, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic after set; override in subclass if needed + } + + /** + * Replaces all store data or indexes with new data for bulk operations + * @param {Array} data - Data to replace with (format depends on type) + * @param {string} [type=STRING_RECORDS] - Type of data: 'records' or 'indexes' + * @returns {boolean} True if operation succeeded + * @throws {Error} Throws error if type is invalid + * @example + * const records = [['key1', {name: 'John'}], ['key2', {name: 'Jane'}]]; + * store.override(records, 'records'); + */ override (data, type = STRING_RECORDS) { const result = true; - if (type === STRING_INDEXES) { this.indexes = new Map(data.map(i => [i[0], new Map(i[1].map(ii => [ii[0], new Set(ii[1])]))])); } else if (type === STRING_RECORDS) { @@ -297,65 +620,109 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } else { throw new Error(STRING_INVALID_TYPE); } - this.onoverride(type); return result; } - reduce (fn, accumulator, raw = false) { - let a = accumulator ?? this.data.keys().next().value; - + /** + * Reduces all records to a single value using a reducer function + * @param {Function} fn - Reducer function (accumulator, value, key, store) + * @param {*} [accumulator] - Initial accumulator value + * @returns {*} Final reduced value + * @example + * const totalAge = store.reduce((sum, record) => sum + record.age, 0); + * const names = store.reduce((acc, record) => acc.concat(record.name), []); + */ + reduce (fn, accumulator = []) { + let a = accumulator; this.forEach((v, k) => { - a = fn(a, v, k, this, raw); + a = fn(a, v, k, this); }, this); return a; } + /** + * Rebuilds indexes for specified fields or all fields for data consistency + * @param {string|string[]} [index] - Specific index field(s) to rebuild, or all if not specified + * @returns {Haro} This instance for method chaining + * @example + * store.reindex(); // Rebuild all indexes + * store.reindex('name'); // Rebuild only name index + * store.reindex(['name', 'email']); // Rebuild name and email indexes + */ reindex (index) { const indices = index ? [index] : this.index; - if (index && this.index.includes(index) === false) { this.index.push(index); } - this.each(indices, i => this.indexes.set(i, new Map())); - this.forEach((data, key) => this.each(indices, i => this.setIndex(this.index, this.indexes, this.delimiter, key, data, i))); + this.forEach((data, key) => this.each(indices, i => this.setIndex(key, data, i))); return this; } + /** + * Searches for records containing a value across specified indexes + * @param {*} value - Value to search for (string, function, or RegExp) + * @param {string|string[]} [index] - Index(es) to search in, or all if not specified + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of matching records + * @example + * const results = store.search('john'); // Search all indexes + * const nameResults = store.search('john', 'name'); // Search only name index + * const regexResults = store.search(/^admin/, 'role'); // Regex search + */ search (value, index, raw = false) { - const result = new Map(), - fn = typeof value === STRING_FUNCTION, - rgex = value && typeof value.test === STRING_FUNCTION; - - if (value) { - this.each(index ? Array.isArray(index) ? index : [index] : this.index, i => { - let idx = this.indexes.get(i); - - if (idx) { - idx.forEach((lset, lkey) => { - switch (true) { - case fn && value(lkey, i): - case rgex && value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey): - case lkey === value: - lset.forEach(key => { - if (result.has(key) === false && this.data.has(key)) { - result.set(key, this.get(key, raw)); - } - }); - break; + const result = new Set(); // Use Set for unique keys + const fn = typeof value === STRING_FUNCTION; + const rgex = value && typeof value.test === STRING_FUNCTION; + if (!value) return this.immutable ? this.freeze() : []; + const indices = index ? Array.isArray(index) ? index : [index] : this.index; + for (const i of indices) { + const idx = this.indexes.get(i); + if (idx) { + for (const [lkey, lset] of idx) { + let match = false; + + if (fn) { + match = value(lkey, i); + } else if (rgex) { + match = value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey); + } else { + match = lkey === value; + } + + if (match) { + for (const key of lset) { + if (this.data.has(key)) { + result.add(key); + } } - }); + } } - }); + } + } + let records = Array.from(result).map(key => this.get(key, raw)); + if (!raw && this.immutable) { + records = Object.freeze(records); } - return raw ? Array.from(result.values()) : this.list(...Array.from(result.values())); + return records; } + /** + * Sets or updates a record in the store with automatic indexing + * @param {string|null} [key=null] - Key for the record, or null to use record's key field + * @param {Object} [data={}] - Record data to set + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @param {boolean} [override=false] - Whether to override existing data instead of merging + * @returns {Object} The stored record (frozen if immutable mode) + * @example + * const user = store.set(null, {name: 'John', age: 30}); // Auto-generate key + * const updated = store.set('user123', {age: 31}); // Update existing record + */ set (key = null, data = {}, batch = false, override = false) { if (key === null) { key = data[this.key] ?? this.uuid(); @@ -368,7 +735,7 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } } else { const og = this.get(key, true); - this.delIndex(this.index, this.indexes, this.delimiter, key, og); + this.deleteIndex(key, og); if (this.versioning) { this.versions.get(key).add(Object.freeze(this.clone(og))); } @@ -377,66 +744,128 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : } } this.data.set(key, x); - this.setIndex(this.index, this.indexes, this.delimiter, key, x, null); + this.setIndex(key, x, null); const result = this.get(key); this.onset(result, batch); return result; } - setIndex (index, indexes, delimiter, key, data, indice) { - this.each(indice === null ? index : [indice], i => { - let lindex = indexes.get(i); - if (!lindex) { - lindex = new Map(); - indexes.set(i, lindex); + /** + * Internal method to add entries to indexes for a record + * @param {string} key - Key of record being indexed + * @param {Object} data - Data of record being indexed + * @param {string|null} indice - Specific index to update, or null for all + * @returns {Haro} This instance for method chaining + */ + setIndex (key, data, indice) { + this.each(indice === null ? this.index : [indice], i => { + let idx = this.indexes.get(i); + if (!idx) { + idx = new Map(); + this.indexes.set(i, idx); } - if (i.includes(delimiter)) { - this.each(this.indexKeys(i, delimiter, data), c => { - if (!lindex.has(c)) { - lindex.set(c, new Set()); - } - lindex.get(c).add(key); - }); + const fn = c => { + if (!idx.has(c)) { + idx.set(c, new Set()); + } + idx.get(c).add(key); + }; + if (i.includes(this.delimiter)) { + this.each(this.indexKeys(i, this.delimiter, data), fn); } else { - this.each(Array.isArray(data[i]) ? data[i] : [data[i]], d => { - if (!lindex.has(d)) { - lindex.set(d, new Set()); - } - lindex.get(d).add(key); - }); + this.each(Array.isArray(data[i]) ? data[i] : [data[i]], fn); } }); + + return this; } - sort (fn, frozen = true) { - return frozen ? Object.freeze(this.limit(INT_0, this.data.size, true).sort(fn).map(i => Object.freeze(i))) : this.limit(INT_0, this.data.size, true).sort(fn); + /** + * Sorts all records using a comparator function + * @param {Function} fn - Comparator function for sorting (a, b) => number + * @param {boolean} [frozen=false] - Whether to return frozen records + * @returns {Array} Sorted array of records + * @example + * const sorted = store.sort((a, b) => a.age - b.age); // Sort by age + * const names = store.sort((a, b) => a.name.localeCompare(b.name)); // Sort by name + */ + sort (fn, frozen = false) { + const dataSize = this.data.size; + let result = this.limit(INT_0, dataSize, true).sort(fn); + if (frozen) { + result = this.freeze(...result); + } + + return result; } + /** + * Comparator function for sorting keys with type-aware comparison logic + * @param {*} a - First value to compare + * @param {*} b - Second value to compare + * @returns {number} Negative number if a < b, positive if a > b, zero if equal + * @example + * const keys = ['name', 'age', 'email']; + * keys.sort(store.sortKeys); // Alphabetical sort + * + * const mixed = [10, '5', 'abc', 3]; + * mixed.sort(store.sortKeys); // Type-aware sort: numbers first, then strings + */ + sortKeys (a, b) { + // Handle string comparison + if (typeof a === STRING_STRING && typeof b === STRING_STRING) { + return a.localeCompare(b); + } + // Handle numeric comparison + if (typeof a === STRING_NUMBER && typeof b === STRING_NUMBER) { + return a - b; + } + + // Handle mixed types or other types by converting to string + + return String(a).localeCompare(String(b)); + } + + /** + * Sorts records by a specific indexed field in ascending order + * @param {string} [index=STRING_EMPTY] - Index field name to sort by + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records sorted by the specified field + * @throws {Error} Throws error if index field is empty or invalid + * @example + * const byAge = store.sortBy('age'); + * const byName = store.sortBy('name'); + */ sortBy (index = STRING_EMPTY, raw = false) { if (index === STRING_EMPTY) { throw new Error(STRING_INVALID_FIELD); } - - const result = [], - keys = []; - + let result = []; + const keys = []; if (this.indexes.has(index) === false) { this.reindex(index); } - const lindex = this.indexes.get(index); - lindex.forEach((idx, key) => keys.push(key)); - this.each(keys.sort(), i => lindex.get(i).forEach(key => result.push(this.get(key, raw)))); + this.each(keys.sort(this.sortKeys), i => lindex.get(i).forEach(key => result.push(this.get(key, raw)))); + if (this.immutable) { + result = Object.freeze(result); + } - return raw ? result : this.list(...result); + return result; } - toArray (frozen = true) { + /** + * Converts all store data to a plain array of records + * @returns {Array} Array containing all records in the store + * @example + * const allRecords = store.toArray(); + * console.log(`Store contains ${allRecords.length} records`); + */ + toArray () { const result = Array.from(this.data.values()); - - if (frozen) { + if (this.immutable) { this.each(result, i => Object.freeze(i)); Object.freeze(result); } @@ -444,61 +873,142 @@ const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : return result; } + /** + * Generates a RFC4122 v4 UUID for record identification + * @returns {string} UUID string in standard format + * @example + * const id = store.uuid(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479" + */ uuid () { - return uuid(); + return crypto.randomUUID(); } + /** + * Returns an iterator of all values in the store + * @returns {Iterator} Iterator of record values + * @example + * for (const record of store.values()) { + * console.log(record.name); + * } + */ values () { return this.data.values(); } - where (predicate = {}, raw = false, op = STRING_DOUBLE_PIPE) { - const keys = this.index.filter(i => i in predicate); + /** + * Internal helper method for predicate matching with support for arrays and regex + * @param {Object} record - Record to test against predicate + * @param {Object} predicate - Predicate object with field-value pairs + * @param {string} op - Operator for array matching ('||' for OR, '&&' for AND) + * @returns {boolean} True if record matches predicate criteria + */ + matchesPredicate (record, predicate, op) { + const keys = Object.keys(predicate); + + return keys.every(key => { + const pred = predicate[key]; + const val = record[key]; + if (Array.isArray(pred)) { + if (Array.isArray(val)) { + return op === STRING_DOUBLE_AND ? pred.every(p => val.includes(p)) : pred.some(p => val.includes(p)); + } else { + return op === STRING_DOUBLE_AND ? pred.every(p => val === p) : pred.some(p => val === p); + } + } else if (pred instanceof RegExp) { + if (Array.isArray(val)) { + return op === STRING_DOUBLE_AND ? val.every(v => pred.test(v)) : val.some(v => pred.test(v)); + } else { + return pred.test(val); + } + } else if (Array.isArray(val)) { + return val.includes(pred); + } else { + return val === pred; + } + }); + } + /** + * Advanced filtering with predicate logic supporting AND/OR operations on arrays + * @param {Object} [predicate={}] - Object with field-value pairs for filtering + * @param {string} [op=STRING_DOUBLE_PIPE] - Operator for array matching ('||' for OR, '&&' for AND) + * @returns {Array} Array of records matching the predicate criteria + * @example + * // Find records with tags containing 'admin' OR 'user' + * const users = store.where({tags: ['admin', 'user']}, '||'); + * + * // Find records with ALL specified tags + * const powerUsers = store.where({tags: ['admin', 'power']}, '&&'); + * + * // Regex matching + * const emails = store.where({email: /^admin@/}); + */ + where (predicate = {}, op = STRING_DOUBLE_PIPE) { + const keys = this.index.filter(i => i in predicate); if (keys.length === 0) return []; - // Supported operators: '||' (OR), '&&' (AND) - // Always AND across fields (all keys must match for a record) - return this.filter(a => { - const matches = keys.map(i => { - const pred = predicate[i]; - const val = a[i]; + // Try to use indexes for better performance + const indexedKeys = keys.filter(k => this.indexes.has(k)); + if (indexedKeys.length > 0) { + // Use index-based filtering for better performance + let candidateKeys = new Set(); + let first = true; + for (const key of indexedKeys) { + const pred = predicate[key]; + const idx = this.indexes.get(key); + const matchingKeys = new Set(); if (Array.isArray(pred)) { - if (Array.isArray(val)) { - if (op === "&&") { - return pred.every(p => val.includes(p)); - } else { - return pred.some(p => val.includes(p)); + for (const p of pred) { + if (idx.has(p)) { + for (const k of idx.get(p)) { + matchingKeys.add(k); + } } - } else if (op === "&&") { - return pred.every(p => val === p); - } else { - return pred.some(p => val === p); } - } else if (pred instanceof RegExp) { - if (Array.isArray(val)) { - if (op === "&&") { - return val.every(v => pred.test(v)); - } else { - return val.some(v => pred.test(v)); - } - } else { - return pred.test(val); + } else if (idx.has(pred)) { + for (const k of idx.get(pred)) { + matchingKeys.add(k); } - } else if (Array.isArray(val)) { - return val.includes(pred); + } + if (first) { + candidateKeys = matchingKeys; + first = false; } else { - return val === pred; + // AND operation across different fields + candidateKeys = new Set([...candidateKeys].filter(k => matchingKeys.has(k))); } - }); - const isMatch = matches.every(Boolean); + } + // Filter candidates with full predicate logic + const results = []; + for (const key of candidateKeys) { + const record = this.get(key, true); + if (this.matchesPredicate(record, predicate, op)) { + results.push(this.immutable ? this.get(key) : record); + } + } - return isMatch; - }, raw); - } + return this.immutable ? this.freeze(...results) : results; + } + // Fallback to full scan if no indexes available + return this.filter(a => this.matchesPredicate(a, predicate, op)); + } } +/** + * Factory function to create a new Haro instance with optional initial data + * @param {Array|null} [data=null] - Initial data to populate the store + * @param {Object} [config={}] - Configuration object passed to Haro constructor + * @returns {Haro} New Haro instance configured and optionally populated + * @example + * const store = haro([ + * {id: 1, name: 'John', age: 30}, + * {id: 2, name: 'Jane', age: 25} + * ], { + * index: ['name', 'age'], + * versioning: true + * }); + */ function haro (data = null, config = {}) { const obj = new Haro(config); diff --git a/dist/haro.umd.min.js b/dist/haro.umd.min.js index dbfc5ad8..6277f793 100644 --- a/dist/haro.umd.min.js +++ b/dist/haro.umd.min.js @@ -1,5 +1,5 @@ /*! 2025 Jason Mulligan - @version 15.2.6 + @version 16.0.0 */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).lru={})}(this,(function(e){"use strict";const t="",s="function",r="Invalid function",i="records",n=[8,9,"a","b"];function h(){return(65536*(Math.random()+1)|0).toString(16).substring(1)}const a="object"==typeof crypto?crypto.randomUUID.bind(crypto):function(){return`${h()}${h()}-${h()}-4${h().slice(0,3)}-${n[Math.floor(4*Math.random())]}${h().slice(0,3)}-${h()}${h()}${h()}`};class o{constructor({delimiter:e="|",id:t=this.uuid(),index:s=[],key:r="id",versioning:i=!1}={}){return this.data=new Map,this.delimiter=e,this.id=t,this.index=Array.isArray(s)?[...s]:[],this.indexes=new Map,this.key=r,this.versions=new Map,this.versioning=i,Object.defineProperty(this,"registry",{enumerable:!0,get:()=>Array.from(this.data.keys())}),Object.defineProperty(this,"size",{enumerable:!0,get:()=>this.data.size}),this.reindex()}batch(e,t="set"){const s="del"===t?e=>this.del(e,!0):e=>this.set(null,e,!0,!0);return this.onbatch(this.beforeBatch(e,t).map(s),t)}beforeBatch(e,t=""){return e}beforeClear(){}beforeDelete(e="",t=!1){return[e,t]}beforeSet(e="",t=!1){return[e,t]}clear(){return this.beforeClear(),this.data.clear(),this.indexes.clear(),this.versions.clear(),this.reindex().onclear(),this}clone(e){return JSON.parse(JSON.stringify(e))}del(e="",t=!1){if(!this.data.has(e))throw new Error("Record not found");const s=this.get(e,!0);this.beforeDelete(e,t),this.delIndex(this.index,this.indexes,this.delimiter,e,s),this.data.delete(e),this.ondelete(e,t),this.versioning&&this.versions.delete(e)}delIndex(e,t,s,r,i){e.forEach((e=>{const n=t.get(e);if(!n)return;const h=e.includes(s)?this.indexKeys(e,s,i):Array.isArray(i[e])?i[e]:[i[e]];this.each(h,(e=>{if(n.has(e)){const t=n.get(e);t.delete(r),0===t.size&&n.delete(e)}}))}))}dump(e=i){let t;return t=e===i?Array.from(this.entries()):Array.from(this.indexes).map((e=>(e[1]=Array.from(e[1]).map((e=>(e[1]=Array.from(e[1]),e))),e))),t}each(e=[],t){for(const[s,r]of e.entries())t(r,s);return e}entries(){return this.data.entries()}find(e={},t=!1){const s=Object.keys(e).sort(((e,t)=>e.localeCompare(t))).join(this.delimiter),r=this.indexes.get(s)??new Map;let i=[];if(r.size>0){const n=this.indexKeys(s,this.delimiter,e);i=Array.from(n.reduce(((e,t)=>(r.has(t)&&r.get(t).forEach((t=>e.add(t))),e)),new Set)).map((e=>this.get(e,t)))}return t?i:this.list(...i)}filter(e,t=!1){if(typeof e!==s)throw new Error(r);const i=t?(e,t)=>t:(e,t)=>Object.freeze([e,Object.freeze(t)]),n=this.reduce(((t,s,r,n)=>(e.call(n,s)&&t.push(i(r,s)),t)),[]);return t?n:Object.freeze(n)}forEach(e,t){return this.data.forEach(((t,s)=>e(this.clone(t),this.clone(s))),t??this.data),this}get(e,t=!1){const s=this.clone(this.data.get(e)??null);return t?s:this.list(e,s)}has(e){return this.data.has(e)}indexKeys(e="",t="|",s={}){return e.split(t).reduce(((e,r,i)=>{const n=[];return(Array.isArray(s[r])?s[r]:[s[r]]).forEach((s=>0===i?n.push(s):e.forEach((e=>n.push(`${e}${t}${s}`))))),n}),[])}keys(){return this.data.keys()}limit(e=0,t=0,s=!1){const r=this.registry.slice(e,e+t).map((e=>this.get(e,s)));return s?r:this.list(...r)}list(...e){return Object.freeze(e.map((e=>Object.freeze(e))))}map(e,t=!1){if(typeof e!==s)throw new Error(r);const i=[];return this.forEach(((t,s)=>i.push(e(t,s)))),t?i:this.list(...i)}merge(e,t,s=!1){return Array.isArray(e)&&Array.isArray(t)?e=s?t:e.concat(t):"object"==typeof e&&null!==e&&"object"==typeof t&&null!==t?this.each(Object.keys(t),(r=>{e[r]=this.merge(e[r],t[r],s)})):e=t,e}onbatch(e,t=""){return e}onclear(){}ondelete(e="",t=!1){return[e,t]}onoverride(e=""){return e}onset(e={},t=!1){return[e,t]}override(e,t=i){if("indexes"===t)this.indexes=new Map(e.map((e=>[e[0],new Map(e[1].map((e=>[e[0],new Set(e[1])])))])));else{if(t!==i)throw new Error("Invalid type");this.indexes.clear(),this.data=new Map(e)}return this.onoverride(t),!0}reduce(e,t,s=!1){let r=t??this.data.keys().next().value;return this.forEach(((t,i)=>{r=e(r,t,i,this,s)}),this),r}reindex(e){const t=e?[e]:this.index;return e&&!1===this.index.includes(e)&&this.index.push(e),this.each(t,(e=>this.indexes.set(e,new Map))),this.forEach(((e,s)=>this.each(t,(t=>this.setIndex(this.index,this.indexes,this.delimiter,s,e,t))))),this}search(e,t,r=!1){const i=new Map,n=typeof e===s,h=e&&typeof e.test===s;return e&&this.each(t?Array.isArray(t)?t:[t]:this.index,(t=>{let s=this.indexes.get(t);s&&s.forEach(((s,a)=>{switch(!0){case n&&e(a,t):case h&&e.test(Array.isArray(a)?a.join(","):a):case a===e:s.forEach((e=>{!1===i.has(e)&&this.data.has(e)&&i.set(e,this.get(e,r))}))}}))})),r?Array.from(i.values()):this.list(...Array.from(i.values()))}set(e=null,t={},s=!1,r=!1){null===e&&(e=t[this.key]??this.uuid());let i={...t,[this.key]:e};if(this.beforeSet(e,i,s,r),this.data.has(e)){const t=this.get(e,!0);this.delIndex(this.index,this.indexes,this.delimiter,e,t),this.versioning&&this.versions.get(e).add(Object.freeze(this.clone(t))),r||(i=this.merge(this.clone(t),i))}else this.versioning&&this.versions.set(e,new Set);this.data.set(e,i),this.setIndex(this.index,this.indexes,this.delimiter,e,i,null);const n=this.get(e);return this.onset(n,s),n}setIndex(e,t,s,r,i,n){this.each(null===n?e:[n],(e=>{let n=t.get(e);n||(n=new Map,t.set(e,n)),e.includes(s)?this.each(this.indexKeys(e,s,i),(e=>{n.has(e)||n.set(e,new Set),n.get(e).add(r)})):this.each(Array.isArray(i[e])?i[e]:[i[e]],(e=>{n.has(e)||n.set(e,new Set),n.get(e).add(r)}))}))}sort(e,t=!0){return t?Object.freeze(this.limit(0,this.data.size,!0).sort(e).map((e=>Object.freeze(e)))):this.limit(0,this.data.size,!0).sort(e)}sortBy(e="",s=!1){if(e===t)throw new Error("Invalid field");const r=[],i=[];!1===this.indexes.has(e)&&this.reindex(e);const n=this.indexes.get(e);return n.forEach(((e,t)=>i.push(t))),this.each(i.sort(),(e=>n.get(e).forEach((e=>r.push(this.get(e,s)))))),s?r:this.list(...r)}toArray(e=!0){const t=Array.from(this.data.values());return e&&(this.each(t,(e=>Object.freeze(e))),Object.freeze(t)),t}uuid(){return a()}values(){return this.data.values()}where(e={},t=!1,s="||"){const r=this.index.filter((t=>t in e));return 0===r.length?[]:this.filter((t=>r.map((r=>{const i=e[r],n=t[r];return Array.isArray(i)?Array.isArray(n)?"&&"===s?i.every((e=>n.includes(e))):i.some((e=>n.includes(e))):"&&"===s?i.every((e=>n===e)):i.some((e=>n===e)):i instanceof RegExp?Array.isArray(n)?"&&"===s?n.every((e=>i.test(e))):n.some((e=>i.test(e))):i.test(n):Array.isArray(n)?n.includes(i):n===i})).every(Boolean)),t)}}e.Haro=o,e.haro=function(e=null,t={}){const s=new o(t);return Array.isArray(e)&&s.batch(e,"set"),s}}));//# sourceMappingURL=haro.umd.min.js.map +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("crypto")):"function"==typeof define&&define.amd?define(["exports","crypto"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).lru={},e.crypto)}(this,function(e,t){"use strict";const s="",r="&&",i="function",n="object",h="records",a="string",o="number",l="Invalid function";class c{constructor({delimiter:e="|",id:t=this.uuid(),immutable:s=!1,index:r=[],key:i="id",versioning:n=!1}={}){return this.data=new Map,this.delimiter=e,this.id=t,this.immutable=s,this.index=Array.isArray(r)?[...r]:[],this.indexes=new Map,this.key=i,this.versions=new Map,this.versioning=n,Object.defineProperty(this,"registry",{enumerable:!0,get:()=>Array.from(this.data.keys())}),Object.defineProperty(this,"size",{enumerable:!0,get:()=>this.data.size}),this.reindex()}batch(e,t="set"){const s="del"===t?e=>this.delete(e,!0):e=>this.set(null,e,!0,!0);return this.onbatch(this.beforeBatch(e,t).map(s),t)}beforeBatch(e,t=""){return e}beforeClear(){}beforeDelete(e="",t=!1){}beforeSet(e="",t={},s=!1,r=!1){}clear(){return this.beforeClear(),this.data.clear(),this.indexes.clear(),this.versions.clear(),this.reindex().onclear(),this}clone(e){return structuredClone(e)}delete(e="",t=!1){if(!this.data.has(e))throw new Error("Record not found");const s=this.get(e,!0);this.beforeDelete(e,t),this.deleteIndex(e,s),this.data.delete(e),this.ondelete(e,t),this.versioning&&this.versions.delete(e)}deleteIndex(e,t){return this.index.forEach(s=>{const r=this.indexes.get(s);if(!r)return;const i=s.includes(this.delimiter)?this.indexKeys(s,this.delimiter,t):Array.isArray(t[s])?t[s]:[t[s]];this.each(i,t=>{if(r.has(t)){const s=r.get(t);s.delete(e),0===s.size&&r.delete(t)}})}),this}dump(e=h){let t;return t=e===h?Array.from(this.entries()):Array.from(this.indexes).map(e=>(e[1]=Array.from(e[1]).map(e=>(e[1]=Array.from(e[1]),e)),e)),t}each(e=[],t){const s=e.length;for(let r=0;r0){const n=this.indexKeys(s,this.delimiter,e);i=Array.from(n.reduce((e,t)=>(r.has(t)&&r.get(t).forEach(t=>e.add(t)),e),new Set)).map(e=>this.get(e,t))}return!t&&this.immutable&&(i=Object.freeze(i)),i}filter(e,t=!1){if(typeof e!==i)throw new Error(l);let s=this.reduce((t,s)=>(e(s)&&t.push(s),t),[]);return t||(s=s.map(e=>this.list(e)),this.immutable&&(s=Object.freeze(s))),s}forEach(e,t=this){return this.data.forEach((s,r)=>{this.immutable&&(s=this.clone(s)),e.call(t,s,r)},this),this}freeze(...e){return Object.freeze(e.map(e=>Object.freeze(e)))}get(e,t=!1){let s=this.data.get(e)??null;return null===s||t||(s=this.list(s),this.immutable&&(s=Object.freeze(s))),s}has(e){return this.data.has(e)}indexKeys(e="",t="|",s={}){const r=e.split(t).sort(this.sortKeys),i=r.length;let n=[""];for(let e=0;ethis.get(e,s));return!s&&this.immutable&&(r=Object.freeze(r)),r}list(e){const t=[e[this.key],e];return this.immutable?this.freeze(...t):t}map(e,t=!1){if(typeof e!==i)throw new Error(l);let s=[];return this.forEach((t,r)=>s.push(e(t,r))),t||(s=s.map(e=>this.list(e)),this.immutable&&(s=Object.freeze(s))),s}merge(e,t,s=!1){return Array.isArray(e)&&Array.isArray(t)?e=s?t:e.concat(t):typeof e===n&&null!==e&&typeof t===n&&null!==t?this.each(Object.keys(t),r=>{e[r]=this.merge(e[r],t[r],s)}):e=t,e}onbatch(e,t=""){return e}onclear(){}ondelete(e="",t=!1){}onoverride(e=""){}onset(e={},t=!1){}override(e,t=h){if("indexes"===t)this.indexes=new Map(e.map(e=>[e[0],new Map(e[1].map(e=>[e[0],new Set(e[1])]))]));else{if(t!==h)throw new Error("Invalid type");this.indexes.clear(),this.data=new Map(e)}return this.onoverride(t),!0}reduce(e,t=[]){let s=t;return this.forEach((t,r)=>{s=e(s,t,r,this)},this),s}reindex(e){const t=e?[e]:this.index;return e&&!1===this.index.includes(e)&&this.index.push(e),this.each(t,e=>this.indexes.set(e,new Map)),this.forEach((e,s)=>this.each(t,t=>this.setIndex(s,e,t))),this}search(e,t,s=!1){const r=new Set,n=typeof e===i,h=e&&typeof e.test===i;if(!e)return this.immutable?this.freeze():[];const a=t?Array.isArray(t)?t:[t]:this.index;for(const t of a){const s=this.indexes.get(t);if(s)for(const[i,a]of s){let s=!1;if(s=n?e(i,t):h?e.test(Array.isArray(i)?i.join(","):i):i===e,s)for(const e of a)this.data.has(e)&&r.add(e)}}let o=Array.from(r).map(e=>this.get(e,s));return!s&&this.immutable&&(o=Object.freeze(o)),o}set(e=null,t={},s=!1,r=!1){null===e&&(e=t[this.key]??this.uuid());let i={...t,[this.key]:e};if(this.beforeSet(e,i,s,r),this.data.has(e)){const t=this.get(e,!0);this.deleteIndex(e,t),this.versioning&&this.versions.get(e).add(Object.freeze(this.clone(t))),r||(i=this.merge(this.clone(t),i))}else this.versioning&&this.versions.set(e,new Set);this.data.set(e,i),this.setIndex(e,i,null);const n=this.get(e);return this.onset(n,s),n}setIndex(e,t,s){return this.each(null===s?this.index:[s],s=>{let r=this.indexes.get(s);r||(r=new Map,this.indexes.set(s,r));const i=t=>{r.has(t)||r.set(t,new Set),r.get(t).add(e)};s.includes(this.delimiter)?this.each(this.indexKeys(s,this.delimiter,t),i):this.each(Array.isArray(t[s])?t[s]:[t[s]],i)}),this}sort(e,t=!1){const s=this.data.size;let r=this.limit(0,s,!0).sort(e);return t&&(r=this.freeze(...r)),r}sortKeys(e,t){return typeof e===a&&typeof t===a?e.localeCompare(t):typeof e===o&&typeof t===o?e-t:String(e).localeCompare(String(t))}sortBy(e="",t=!1){if(e===s)throw new Error("Invalid field");let r=[];const i=[];!1===this.indexes.has(e)&&this.reindex(e);const n=this.indexes.get(e);return n.forEach((e,t)=>i.push(t)),this.each(i.sort(this.sortKeys),e=>n.get(e).forEach(e=>r.push(this.get(e,t)))),this.immutable&&(r=Object.freeze(r)),r}toArray(){const e=Array.from(this.data.values());return this.immutable&&(this.each(e,e=>Object.freeze(e)),Object.freeze(e)),e}uuid(){return t.randomUUID()}values(){return this.data.values()}matchesPredicate(e,t,s){return Object.keys(t).every(i=>{const n=t[i],h=e[i];return Array.isArray(n)?Array.isArray(h)?s===r?n.every(e=>h.includes(e)):n.some(e=>h.includes(e)):s===r?n.every(e=>h===e):n.some(e=>h===e):n instanceof RegExp?Array.isArray(h)?s===r?h.every(e=>n.test(e)):h.some(e=>n.test(e)):n.test(h):Array.isArray(h)?h.includes(n):h===n})}where(e={},t="||"){const s=this.index.filter(t=>t in e);if(0===s.length)return[];const r=s.filter(e=>this.indexes.has(e));if(r.length>0){let s=new Set,i=!0;for(const t of r){const r=e[t],n=this.indexes.get(t),h=new Set;if(Array.isArray(r)){for(const e of r)if(n.has(e))for(const t of n.get(e))h.add(t)}else if(n.has(r))for(const e of n.get(r))h.add(e);i?(s=h,i=!1):s=new Set([...s].filter(e=>h.has(e)))}const n=[];for(const r of s){const s=this.get(r,!0);this.matchesPredicate(s,e,t)&&n.push(this.immutable?this.get(r):s)}return this.immutable?this.freeze(...n):n}return this.filter(s=>this.matchesPredicate(s,e,t))}}e.Haro=c,e.haro=function(e=null,t={}){const s=new c(t);return Array.isArray(e)&&s.batch(e,"set"),s}});//# sourceMappingURL=haro.umd.min.js.map diff --git a/dist/haro.umd.min.js.map b/dist/haro.umd.min.js.map index 00365e13..46b6d34e 100644 --- a/dist/haro.umd.min.js.map +++ b/dist/haro.umd.min.js.map @@ -1 +1 @@ -{"version":3,"file":"haro.umd.min.js","sources":["../src/constants.js","../src/uuid.js","../src/haro.js"],"sourcesContent":["export const STRING_COMMA = \",\";\nexport const STRING_EMPTY = \"\";\nexport const STRING_PIPE = \"|\";\nexport const STRING_DOUBLE_PIPE = \"||\";\nexport const STRING_A = \"a\";\nexport const STRING_B = \"b\";\nexport const STRING_DEL = \"del\";\nexport const STRING_FUNCTION = \"function\";\nexport const STRING_INDEXES = \"indexes\";\nexport const STRING_INVALID_FIELD = \"Invalid field\";\nexport const STRING_INVALID_FUNCTION = \"Invalid function\";\nexport const STRING_INVALID_TYPE = \"Invalid type\";\nexport const STRING_OBJECT = \"object\";\nexport const STRING_RECORD_NOT_FOUND = \"Record not found\";\nexport const STRING_RECORDS = \"records\";\nexport const STRING_REGISTRY = \"registry\";\nexport const STRING_SET = \"set\";\nexport const STRING_SIZE = \"size\";\nexport const INT_0 = 0;\nexport const INT_1 = 1;\nexport const INT_3 = 3;\nexport const INT_4 = 4;\nexport const INT_8 = 8;\nexport const INT_9 = 9;\nexport const INT_16 = 16;\n","import {INT_0, INT_1, INT_16, INT_3, INT_4, INT_8, INT_9, STRING_A, STRING_B, STRING_OBJECT} from \"./constants.js\";\n\n/* istanbul ignore next */\nconst r = [INT_8, INT_9, STRING_A, STRING_B];\n\n/* istanbul ignore next */\nfunction s () {\n\treturn ((Math.random() + INT_1) * 0x10000 | INT_0).toString(INT_16).substring(INT_1);\n}\n\n/* istanbul ignore next */\nfunction randomUUID () {\n\treturn `${s()}${s()}-${s()}-4${s().slice(INT_0, INT_3)}-${r[Math.floor(Math.random() * INT_4)]}${s().slice(INT_0, INT_3)}-${s()}${s()}${s()}`;\n}\n\nexport const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : randomUUID;\n","import {uuid} from \"./uuid.js\";\nimport {\n\tINT_0,\n\tSTRING_COMMA,\n\tSTRING_DEL,\n\tSTRING_DOUBLE_PIPE,\n\tSTRING_EMPTY,\n\tSTRING_FUNCTION,\n\tSTRING_INDEXES,\n\tSTRING_INVALID_FIELD,\n\tSTRING_INVALID_FUNCTION,\n\tSTRING_INVALID_TYPE,\n\tSTRING_PIPE,\n\tSTRING_RECORD_NOT_FOUND,\n\tSTRING_RECORDS,\n\tSTRING_REGISTRY,\n\tSTRING_SET,\n\tSTRING_SIZE\n} from \"./constants.js\";\n\nexport class Haro {\n\tconstructor ({delimiter = STRING_PIPE, id = this.uuid(), index = [], key = \"id\", versioning = false} = {}) {\n\t\tthis.data = new Map();\n\t\tthis.delimiter = delimiter;\n\t\tthis.id = id;\n\t\tthis.index = Array.isArray(index) ? [...index] : [];\n\t\tthis.indexes = new Map();\n\t\tthis.key = key;\n\t\tthis.versions = new Map();\n\t\tthis.versioning = versioning;\n\n\t\tObject.defineProperty(this, STRING_REGISTRY, {\n\t\t\tenumerable: true,\n\t\t\tget: () => Array.from(this.data.keys())\n\t\t});\n\t\tObject.defineProperty(this, STRING_SIZE, {\n\t\t\tenumerable: true,\n\t\t\tget: () => this.data.size\n\t\t});\n\n\t\treturn this.reindex();\n\t}\n\n\tbatch (args, type = STRING_SET) {\n\t\tconst fn = type === STRING_DEL ? i => this.del(i, true) : i => this.set(null, i, true, true);\n\n\t\treturn this.onbatch(this.beforeBatch(args, type).map(fn), type);\n\t}\n\n\tbeforeBatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\treturn arg;\n\t}\n\n\tbeforeClear () {\n\t\t// Hook for custom logic before clear; override in subclass if needed\n\t}\n\n\tbeforeDelete (key = STRING_EMPTY, batch = false) {\n\t\treturn [key, batch];\n\t}\n\n\tbeforeSet (key = STRING_EMPTY, batch = false) {\n\t\treturn [key, batch];\n\t}\n\n\tclear () {\n\t\tthis.beforeClear();\n\t\tthis.data.clear();\n\t\tthis.indexes.clear();\n\t\tthis.versions.clear();\n\t\tthis.reindex().onclear();\n\n\t\treturn this;\n\t}\n\n\tclone (arg) {\n\t\treturn JSON.parse(JSON.stringify(arg));\n\t}\n\n\tdel (key = STRING_EMPTY, batch = false) {\n\t\tif (!this.data.has(key)) {\n\t\t\tthrow new Error(STRING_RECORD_NOT_FOUND);\n\t\t}\n\t\tconst og = this.get(key, true);\n\t\tthis.beforeDelete(key, batch);\n\t\tthis.delIndex(this.index, this.indexes, this.delimiter, key, og);\n\t\tthis.data.delete(key);\n\t\tthis.ondelete(key, batch);\n\t\tif (this.versioning) {\n\t\t\tthis.versions.delete(key);\n\t\t}\n\t}\n\n\tdelIndex (index, indexes, delimiter, key, data) {\n\t\tindex.forEach(i => {\n\t\t\tconst idx = indexes.get(i);\n\t\t\tif (!idx) return;\n\t\t\tconst values = i.includes(delimiter) ?\n\t\t\t\tthis.indexKeys(i, delimiter, data) :\n\t\t\t\tArray.isArray(data[i]) ? data[i] : [data[i]];\n\t\t\tthis.each(values, value => {\n\t\t\t\tif (idx.has(value)) {\n\t\t\t\t\tconst o = idx.get(value);\n\t\t\t\t\to.delete(key);\n\t\t\t\t\tif (o.size === INT_0) {\n\t\t\t\t\t\tidx.delete(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\tdump (type = STRING_RECORDS) {\n\t\tlet result;\n\n\t\tif (type === STRING_RECORDS) {\n\t\t\tresult = Array.from(this.entries());\n\t\t} else {\n\t\t\tresult = Array.from(this.indexes).map(i => {\n\t\t\t\ti[1] = Array.from(i[1]).map(ii => {\n\t\t\t\t\tii[1] = Array.from(ii[1]);\n\n\t\t\t\t\treturn ii;\n\t\t\t\t});\n\n\t\t\t\treturn i;\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\teach (arr = [], fn) {\n\t\tfor (const [idx, value] of arr.entries()) {\n\t\t\tfn(value, idx);\n\t\t}\n\n\t\treturn arr;\n\t}\n\n\tentries () {\n\t\treturn this.data.entries();\n\t}\n\n\tfind (where = {}, raw = false) {\n\t\tconst key = Object.keys(where).sort((a, b) => a.localeCompare(b)).join(this.delimiter);\n\t\tconst index = this.indexes.get(key) ?? new Map();\n\t\tlet result = [];\n\t\tif (index.size > 0) {\n\t\t\tconst keys = this.indexKeys(key, this.delimiter, where);\n\t\t\tresult = Array.from(keys.reduce((a, v) => {\n\t\t\t\tif (index.has(v)) {\n\t\t\t\t\tindex.get(v).forEach(k => a.add(k));\n\t\t\t\t}\n\n\t\t\t\treturn a;\n\t\t\t}, new Set())).map(i => this.get(i, raw));\n\t\t}\n\n\t\treturn raw ? result : this.list(...result);\n\t}\n\n\tfilter (fn, raw = false) {\n\t\tif (typeof fn !== STRING_FUNCTION) {\n\t\t\tthrow new Error(STRING_INVALID_FUNCTION);\n\t\t}\n\t\tconst x = raw ? (k, v) => v : (k, v) => Object.freeze([k, Object.freeze(v)]);\n\t\tconst result = this.reduce((a, v, k, ctx) => {\n\t\t\tif (fn.call(ctx, v)) {\n\t\t\t\ta.push(x(k, v));\n\t\t\t}\n\n\t\t\treturn a;\n\t\t}, []);\n\n\t\treturn raw ? result : Object.freeze(result);\n\t}\n\n\tforEach (fn, ctx) {\n\t\tthis.data.forEach((value, key) => fn(this.clone(value), this.clone(key)), ctx ?? this.data);\n\n\t\treturn this;\n\t}\n\n\tget (key, raw = false) {\n\t\tconst result = this.clone(this.data.get(key) ?? null);\n\n\t\treturn raw ? result : this.list(key, result);\n\t}\n\n\thas (key) {\n\t\treturn this.data.has(key);\n\t}\n\n\tindexKeys (arg = STRING_EMPTY, delimiter = STRING_PIPE, data = {}) {\n\t\treturn arg.split(delimiter).reduce((a, li, lidx) => {\n\t\t\tconst result = [];\n\n\t\t\t(Array.isArray(data[li]) ? data[li] : [data[li]]).forEach(lli => lidx === INT_0 ? result.push(lli) : a.forEach(x => result.push(`${x}${delimiter}${lli}`)));\n\n\t\t\treturn result;\n\t\t}, []);\n\t}\n\n\tkeys () {\n\t\treturn this.data.keys();\n\t}\n\n\tlimit (offset = INT_0, max = INT_0, raw = false) {\n\t\tconst result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw));\n\n\t\treturn raw ? result : this.list(...result);\n\t}\n\n\tlist (...args) {\n\t\treturn Object.freeze(args.map(i => Object.freeze(i)));\n\t}\n\n\tmap (fn, raw = false) {\n\t\tif (typeof fn !== STRING_FUNCTION) {\n\t\t\tthrow new Error(STRING_INVALID_FUNCTION);\n\t\t}\n\n\t\tconst result = [];\n\n\t\tthis.forEach((value, key) => result.push(fn(value, key)));\n\n\t\treturn raw ? result : this.list(...result);\n\t}\n\n\tmerge (a, b, override = false) {\n\t\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\t\ta = override ? b : a.concat(b);\n\t\t} else if (typeof a === \"object\" && a !== null && typeof b === \"object\" && b !== null) {\n\t\t\tthis.each(Object.keys(b), i => {\n\t\t\t\ta[i] = this.merge(a[i], b[i], override);\n\t\t\t});\n\t\t} else {\n\t\t\ta = b;\n\t\t}\n\n\t\treturn a;\n\t}\n\n\tonbatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\treturn arg;\n\t}\n\n\tonclear () {\n\t\t// Hook for custom logic after clear; override in subclass if needed\n\t}\n\n\tondelete (key = STRING_EMPTY, batch = false) {\n\t\treturn [key, batch];\n\t}\n\n\tonoverride (type = STRING_EMPTY) {\n\t\treturn type;\n\t}\n\n\tonset (arg = {}, batch = false) {\n\t\treturn [arg, batch];\n\t}\n\n\toverride (data, type = STRING_RECORDS) {\n\t\tconst result = true;\n\n\t\tif (type === STRING_INDEXES) {\n\t\t\tthis.indexes = new Map(data.map(i => [i[0], new Map(i[1].map(ii => [ii[0], new Set(ii[1])]))]));\n\t\t} else if (type === STRING_RECORDS) {\n\t\t\tthis.indexes.clear();\n\t\t\tthis.data = new Map(data);\n\t\t} else {\n\t\t\tthrow new Error(STRING_INVALID_TYPE);\n\t\t}\n\n\t\tthis.onoverride(type);\n\n\t\treturn result;\n\t}\n\n\treduce (fn, accumulator, raw = false) {\n\t\tlet a = accumulator ?? this.data.keys().next().value;\n\n\t\tthis.forEach((v, k) => {\n\t\t\ta = fn(a, v, k, this, raw);\n\t\t}, this);\n\n\t\treturn a;\n\t}\n\n\treindex (index) {\n\t\tconst indices = index ? [index] : this.index;\n\n\t\tif (index && this.index.includes(index) === false) {\n\t\t\tthis.index.push(index);\n\t\t}\n\n\t\tthis.each(indices, i => this.indexes.set(i, new Map()));\n\t\tthis.forEach((data, key) => this.each(indices, i => this.setIndex(this.index, this.indexes, this.delimiter, key, data, i)));\n\n\t\treturn this;\n\t}\n\n\tsearch (value, index, raw = false) {\n\t\tconst result = new Map(),\n\t\t\tfn = typeof value === STRING_FUNCTION,\n\t\t\trgex = value && typeof value.test === STRING_FUNCTION;\n\n\t\tif (value) {\n\t\t\tthis.each(index ? Array.isArray(index) ? index : [index] : this.index, i => {\n\t\t\t\tlet idx = this.indexes.get(i);\n\n\t\t\t\tif (idx) {\n\t\t\t\t\tidx.forEach((lset, lkey) => {\n\t\t\t\t\t\tswitch (true) {\n\t\t\t\t\t\t\tcase fn && value(lkey, i):\n\t\t\t\t\t\t\tcase rgex && value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey):\n\t\t\t\t\t\t\tcase lkey === value:\n\t\t\t\t\t\t\t\tlset.forEach(key => {\n\t\t\t\t\t\t\t\t\tif (result.has(key) === false && this.data.has(key)) {\n\t\t\t\t\t\t\t\t\t\tresult.set(key, this.get(key, raw));\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tvoid 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\treturn raw ? Array.from(result.values()) : this.list(...Array.from(result.values()));\n\t}\n\n\tset (key = null, data = {}, batch = false, override = false) {\n\t\tif (key === null) {\n\t\t\tkey = data[this.key] ?? this.uuid();\n\t\t}\n\t\tlet x = {...data, [this.key]: key};\n\t\tthis.beforeSet(key, x, batch, override);\n\t\tif (!this.data.has(key)) {\n\t\t\tif (this.versioning) {\n\t\t\t\tthis.versions.set(key, new Set());\n\t\t\t}\n\t\t} else {\n\t\t\tconst og = this.get(key, true);\n\t\t\tthis.delIndex(this.index, this.indexes, this.delimiter, key, og);\n\t\t\tif (this.versioning) {\n\t\t\t\tthis.versions.get(key).add(Object.freeze(this.clone(og)));\n\t\t\t}\n\t\t\tif (!override) {\n\t\t\t\tx = this.merge(this.clone(og), x);\n\t\t\t}\n\t\t}\n\t\tthis.data.set(key, x);\n\t\tthis.setIndex(this.index, this.indexes, this.delimiter, key, x, null);\n\t\tconst result = this.get(key);\n\t\tthis.onset(result, batch);\n\n\t\treturn result;\n\t}\n\n\tsetIndex (index, indexes, delimiter, key, data, indice) {\n\t\tthis.each(indice === null ? index : [indice], i => {\n\t\t\tlet lindex = indexes.get(i);\n\t\t\tif (!lindex) {\n\t\t\t\tlindex = new Map();\n\t\t\t\tindexes.set(i, lindex);\n\t\t\t}\n\t\t\tif (i.includes(delimiter)) {\n\t\t\t\tthis.each(this.indexKeys(i, delimiter, data), c => {\n\t\t\t\t\tif (!lindex.has(c)) {\n\t\t\t\t\t\tlindex.set(c, new Set());\n\t\t\t\t\t}\n\t\t\t\t\tlindex.get(c).add(key);\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tthis.each(Array.isArray(data[i]) ? data[i] : [data[i]], d => {\n\t\t\t\t\tif (!lindex.has(d)) {\n\t\t\t\t\t\tlindex.set(d, new Set());\n\t\t\t\t\t}\n\t\t\t\t\tlindex.get(d).add(key);\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t}\n\n\tsort (fn, frozen = true) {\n\t\treturn frozen ? Object.freeze(this.limit(INT_0, this.data.size, true).sort(fn).map(i => Object.freeze(i))) : this.limit(INT_0, this.data.size, true).sort(fn);\n\t}\n\n\tsortBy (index = STRING_EMPTY, raw = false) {\n\t\tif (index === STRING_EMPTY) {\n\t\t\tthrow new Error(STRING_INVALID_FIELD);\n\t\t}\n\n\t\tconst result = [],\n\t\t\tkeys = [];\n\n\t\tif (this.indexes.has(index) === false) {\n\t\t\tthis.reindex(index);\n\t\t}\n\n\t\tconst lindex = this.indexes.get(index);\n\n\t\tlindex.forEach((idx, key) => keys.push(key));\n\t\tthis.each(keys.sort(), i => lindex.get(i).forEach(key => result.push(this.get(key, raw))));\n\n\t\treturn raw ? result : this.list(...result);\n\t}\n\n\ttoArray (frozen = true) {\n\t\tconst result = Array.from(this.data.values());\n\n\t\tif (frozen) {\n\t\t\tthis.each(result, i => Object.freeze(i));\n\t\t\tObject.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tuuid () {\n\t\treturn uuid();\n\t}\n\n\tvalues () {\n\t\treturn this.data.values();\n\t}\n\n\twhere (predicate = {}, raw = false, op = STRING_DOUBLE_PIPE) {\n\t\tconst keys = this.index.filter(i => i in predicate);\n\n\t\tif (keys.length === 0) return [];\n\n\t\t// Supported operators: '||' (OR), '&&' (AND)\n\t\t// Always AND across fields (all keys must match for a record)\n\t\treturn this.filter(a => {\n\t\t\tconst matches = keys.map(i => {\n\t\t\t\tconst pred = predicate[i];\n\t\t\t\tconst val = a[i];\n\t\t\t\tif (Array.isArray(pred)) {\n\t\t\t\t\tif (Array.isArray(val)) {\n\t\t\t\t\t\tif (op === \"&&\") {\n\t\t\t\t\t\t\treturn pred.every(p => val.includes(p));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn pred.some(p => val.includes(p));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (op === \"&&\") {\n\t\t\t\t\t\treturn pred.every(p => val === p);\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn pred.some(p => val === p);\n\t\t\t\t\t}\n\t\t\t\t} else if (pred instanceof RegExp) {\n\t\t\t\t\tif (Array.isArray(val)) {\n\t\t\t\t\t\tif (op === \"&&\") {\n\t\t\t\t\t\t\treturn val.every(v => pred.test(v));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn val.some(v => pred.test(v));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn pred.test(val);\n\t\t\t\t\t}\n\t\t\t\t} else if (Array.isArray(val)) {\n\t\t\t\t\treturn val.includes(pred);\n\t\t\t\t} else {\n\t\t\t\t\treturn val === pred;\n\t\t\t\t}\n\t\t\t});\n\t\t\tconst isMatch = matches.every(Boolean);\n\n\t\t\treturn isMatch;\n\t\t}, raw);\n\t}\n\n}\n\nexport function haro (data = null, config = {}) {\n\tconst obj = new Haro(config);\n\n\tif (Array.isArray(data)) {\n\t\tobj.batch(data, STRING_SET);\n\t}\n\n\treturn obj;\n}\n"],"names":["STRING_EMPTY","STRING_FUNCTION","STRING_INVALID_FUNCTION","STRING_RECORDS","r","s","Math","random","toString","substring","uuid","crypto","randomUUID","bind","slice","floor","Haro","constructor","delimiter","id","this","index","key","versioning","data","Map","Array","isArray","indexes","versions","Object","defineProperty","enumerable","get","from","keys","size","reindex","batch","args","type","fn","i","del","set","onbatch","beforeBatch","map","arg","beforeClear","beforeDelete","beforeSet","clear","onclear","clone","JSON","parse","stringify","has","Error","og","delIndex","delete","ondelete","forEach","idx","values","includes","indexKeys","each","value","o","dump","result","entries","ii","arr","find","where","raw","sort","a","b","localeCompare","join","reduce","v","k","add","Set","list","filter","x","freeze","ctx","call","push","split","li","lidx","lli","limit","offset","max","registry","merge","override","concat","onoverride","onset","accumulator","next","indices","setIndex","search","rgex","test","lset","lkey","indice","lindex","c","d","frozen","sortBy","toArray","predicate","op","length","pred","val","every","p","some","RegExp","Boolean","exports","haro","config","obj"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"haro.umd.min.js","sources":["../src/constants.js","../src/haro.js"],"sourcesContent":["// String constants - Single characters and symbols\nexport const STRING_COMMA = \",\";\nexport const STRING_EMPTY = \"\";\nexport const STRING_PIPE = \"|\";\nexport const STRING_DOUBLE_PIPE = \"||\";\nexport const STRING_DOUBLE_AND = \"&&\";\n\n// String constants - Operation and type names\nexport const STRING_ID = \"id\";\nexport const STRING_DEL = \"del\";\nexport const STRING_FUNCTION = \"function\";\nexport const STRING_INDEXES = \"indexes\";\nexport const STRING_OBJECT = \"object\";\nexport const STRING_RECORDS = \"records\";\nexport const STRING_REGISTRY = \"registry\";\nexport const STRING_SET = \"set\";\nexport const STRING_SIZE = \"size\";\nexport const STRING_STRING = \"string\";\nexport const STRING_NUMBER = \"number\";\n\n// String constants - Error messages\nexport const STRING_INVALID_FIELD = \"Invalid field\";\nexport const STRING_INVALID_FUNCTION = \"Invalid function\";\nexport const STRING_INVALID_TYPE = \"Invalid type\";\nexport const STRING_RECORD_NOT_FOUND = \"Record not found\";\n\n// Integer constants\nexport const INT_0 = 0;\n","import {randomUUID as uuid} from \"crypto\";\nimport {\n\tINT_0,\n\tSTRING_COMMA,\n\tSTRING_DEL, STRING_DOUBLE_AND,\n\tSTRING_DOUBLE_PIPE,\n\tSTRING_EMPTY,\n\tSTRING_FUNCTION,\n\tSTRING_ID,\n\tSTRING_INDEXES,\n\tSTRING_INVALID_FIELD,\n\tSTRING_INVALID_FUNCTION,\n\tSTRING_INVALID_TYPE, STRING_NUMBER, STRING_OBJECT,\n\tSTRING_PIPE,\n\tSTRING_RECORD_NOT_FOUND,\n\tSTRING_RECORDS,\n\tSTRING_REGISTRY,\n\tSTRING_SET,\n\tSTRING_SIZE, STRING_STRING\n} from \"./constants.js\";\n\n/**\n * Haro is a modern immutable DataStore for collections of records with indexing,\n * versioning, and batch operations support. It provides a Map-like interface\n * with advanced querying capabilities through indexes.\n * @class\n * @example\n * const store = new Haro({\n * index: ['name', 'age'],\n * key: 'id',\n * versioning: true\n * });\n *\n * store.set(null, {name: 'John', age: 30});\n * const results = store.find({name: 'John'});\n */\nexport class Haro {\n\t/**\n\t * Creates a new Haro instance with specified configuration\n\t * @param {Object} [config={}] - Configuration object for the store\n\t * @param {string} [config.delimiter=STRING_PIPE] - Delimiter for composite indexes (default: '|')\n\t * @param {string} [config.id] - Unique identifier for this instance (auto-generated if not provided)\n\t * @param {boolean} [config.immutable=false] - Return frozen/immutable objects for data safety\n\t * @param {string[]} [config.index=[]] - Array of field names to create indexes for\n\t * @param {string} [config.key=STRING_ID] - Primary key field name used for record identification\n\t * @param {boolean} [config.versioning=false] - Enable versioning to track record changes\n\t * @constructor\n\t * @example\n\t * const store = new Haro({\n\t * index: ['name', 'email', 'name|department'],\n\t * key: 'userId',\n\t * versioning: true,\n\t * immutable: true\n\t * });\n\t */\n\tconstructor ({delimiter = STRING_PIPE, id = this.uuid(), immutable = false, index = [], key = STRING_ID, versioning = false} = {}) {\n\t\tthis.data = new Map();\n\t\tthis.delimiter = delimiter;\n\t\tthis.id = id;\n\t\tthis.immutable = immutable;\n\t\tthis.index = Array.isArray(index) ? [...index] : [];\n\t\tthis.indexes = new Map();\n\t\tthis.key = key;\n\t\tthis.versions = new Map();\n\t\tthis.versioning = versioning;\n\t\tObject.defineProperty(this, STRING_REGISTRY, {\n\t\t\tenumerable: true,\n\t\t\tget: () => Array.from(this.data.keys())\n\t\t});\n\t\tObject.defineProperty(this, STRING_SIZE, {\n\t\t\tenumerable: true,\n\t\t\tget: () => this.data.size\n\t\t});\n\n\t\treturn this.reindex();\n\t}\n\n\t/**\n\t * Performs batch operations on multiple records for efficient bulk processing\n\t * @param {Array} args - Array of records to process\n\t * @param {string} [type=STRING_SET] - Type of operation: 'set' for upsert, 'del' for delete\n\t * @returns {Array} Array of results from the batch operation\n\t * @throws {Error} Throws error if individual operations fail during batch processing\n\t * @example\n\t * const results = store.batch([\n\t * {id: 1, name: 'John'},\n\t * {id: 2, name: 'Jane'}\n\t * ], 'set');\n\t */\n\tbatch (args, type = STRING_SET) {\n\t\tconst fn = type === STRING_DEL ? i => this.delete(i, true) : i => this.set(null, i, true, true);\n\n\t\treturn this.onbatch(this.beforeBatch(args, type).map(fn), type);\n\t}\n\n\t/**\n\t * Lifecycle hook executed before batch operations for custom preprocessing\n\t * @param {Array} arg - Arguments passed to batch operation\n\t * @param {string} [type=STRING_EMPTY] - Type of batch operation ('set' or 'del')\n\t * @returns {Array} The arguments array (possibly modified) to be processed\n\t */\n\tbeforeBatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic before batch; override in subclass if needed\n\t\treturn arg;\n\t}\n\n\t/**\n\t * Lifecycle hook executed before clear operation for custom preprocessing\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t * @example\n\t * class MyStore extends Haro {\n\t * beforeClear() {\n\t * this.backup = this.toArray();\n\t * }\n\t * }\n\t */\n\tbeforeClear () {\n\t\t// Hook for custom logic before clear; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed before delete operation for custom preprocessing\n\t * @param {string} [key=STRING_EMPTY] - Key of record to delete\n\t * @param {boolean} [batch=false] - Whether this is part of a batch operation\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tbeforeDelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic before delete; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed before set operation for custom preprocessing\n\t * @param {string} [key=STRING_EMPTY] - Key of record to set\n\t * @param {Object} [data={}] - Record data being set\n\t * @param {boolean} [batch=false] - Whether this is part of a batch operation\n\t * @param {boolean} [override=false] - Whether to override existing data\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tbeforeSet (key = STRING_EMPTY, data = {}, batch = false, override = false) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic before set; override in subclass if needed\n\t}\n\n\t/**\n\t * Removes all records, indexes, and versions from the store\n\t * @returns {Haro} This instance for method chaining\n\t * @example\n\t * store.clear();\n\t * console.log(store.size); // 0\n\t */\n\tclear () {\n\t\tthis.beforeClear();\n\t\tthis.data.clear();\n\t\tthis.indexes.clear();\n\t\tthis.versions.clear();\n\t\tthis.reindex().onclear();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Creates a deep clone of the given value, handling objects, arrays, and primitives\n\t * @param {*} arg - Value to clone (any type)\n\t * @returns {*} Deep clone of the argument\n\t * @example\n\t * const original = {name: 'John', tags: ['user', 'admin']};\n\t * const cloned = store.clone(original);\n\t * cloned.tags.push('new'); // original.tags is unchanged\n\t */\n\tclone (arg) {\n\t\treturn structuredClone(arg);\n\t}\n\n\t/**\n\t * Deletes a record from the store and removes it from all indexes\n\t * @param {string} [key=STRING_EMPTY] - Key of record to delete\n\t * @param {boolean} [batch=false] - Whether this is part of a batch operation\n\t * @returns {void}\n\t * @throws {Error} Throws error if record with the specified key is not found\n\t * @example\n\t * store.delete('user123');\n\t * // Throws error if 'user123' doesn't exist\n\t */\n\tdelete (key = STRING_EMPTY, batch = false) {\n\t\tif (!this.data.has(key)) {\n\t\t\tthrow new Error(STRING_RECORD_NOT_FOUND);\n\t\t}\n\t\tconst og = this.get(key, true);\n\t\tthis.beforeDelete(key, batch);\n\t\tthis.deleteIndex(key, og);\n\t\tthis.data.delete(key);\n\t\tthis.ondelete(key, batch);\n\t\tif (this.versioning) {\n\t\t\tthis.versions.delete(key);\n\t\t}\n\t}\n\n\t/**\n\t * Internal method to remove entries from indexes for a deleted record\n\t * @param {string} key - Key of record being deleted\n\t * @param {Object} data - Data of record being deleted\n\t * @returns {Haro} This instance for method chaining\n\t */\n\tdeleteIndex (key, data) {\n\t\tthis.index.forEach(i => {\n\t\t\tconst idx = this.indexes.get(i);\n\t\t\tif (!idx) return;\n\t\t\tconst values = i.includes(this.delimiter) ?\n\t\t\t\tthis.indexKeys(i, this.delimiter, data) :\n\t\t\t\tArray.isArray(data[i]) ? data[i] : [data[i]];\n\t\t\tthis.each(values, value => {\n\t\t\t\tif (idx.has(value)) {\n\t\t\t\t\tconst o = idx.get(value);\n\t\t\t\t\to.delete(key);\n\t\t\t\t\tif (o.size === INT_0) {\n\t\t\t\t\t\tidx.delete(value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Exports complete store data or indexes for persistence or debugging\n\t * @param {string} [type=STRING_RECORDS] - Type of data to export: 'records' or 'indexes'\n\t * @returns {Array} Array of [key, value] pairs for records, or serialized index structure\n\t * @example\n\t * const records = store.dump('records');\n\t * const indexes = store.dump('indexes');\n\t */\n\tdump (type = STRING_RECORDS) {\n\t\tlet result;\n\t\tif (type === STRING_RECORDS) {\n\t\t\tresult = Array.from(this.entries());\n\t\t} else {\n\t\t\tresult = Array.from(this.indexes).map(i => {\n\t\t\t\ti[1] = Array.from(i[1]).map(ii => {\n\t\t\t\t\tii[1] = Array.from(ii[1]);\n\n\t\t\t\t\treturn ii;\n\t\t\t\t});\n\n\t\t\t\treturn i;\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Utility method to iterate over an array with a callback function\n\t * @param {Array<*>} [arr=[]] - Array to iterate over\n\t * @param {Function} fn - Function to call for each element (element, index)\n\t * @returns {Array<*>} The original array for method chaining\n\t * @example\n\t * store.each([1, 2, 3], (item, index) => console.log(item, index));\n\t */\n\teach (arr = [], fn) {\n\t\tconst len = arr.length;\n\t\tfor (let i = 0; i < len; i++) {\n\t\t\tfn(arr[i], i);\n\t\t}\n\n\t\treturn arr;\n\t}\n\n\t/**\n\t * Returns an iterator of [key, value] pairs for each record in the store\n\t * @returns {Iterator>} Iterator of [key, value] pairs\n\t * @example\n\t * for (const [key, value] of store.entries()) {\n\t * console.log(key, value);\n\t * }\n\t */\n\tentries () {\n\t\treturn this.data.entries();\n\t}\n\n\t/**\n\t * Finds records matching the specified criteria using indexes for optimal performance\n\t * @param {Object} [where={}] - Object with field-value pairs to match against\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of matching records (frozen if immutable mode)\n\t * @example\n\t * const users = store.find({department: 'engineering', active: true});\n\t * const admins = store.find({role: 'admin'});\n\t */\n\tfind (where = {}, raw = false) {\n\t\tconst key = Object.keys(where).sort(this.sortKeys).join(this.delimiter);\n\t\tconst index = this.indexes.get(key) ?? new Map();\n\t\tlet result = [];\n\t\tif (index.size > 0) {\n\t\t\tconst keys = this.indexKeys(key, this.delimiter, where);\n\t\t\tresult = Array.from(keys.reduce((a, v) => {\n\t\t\t\tif (index.has(v)) {\n\t\t\t\t\tindex.get(v).forEach(k => a.add(k));\n\t\t\t\t}\n\n\t\t\t\treturn a;\n\t\t\t}, new Set())).map(i => this.get(i, raw));\n\t\t}\n\t\tif (!raw && this.immutable) {\n\t\t\tresult = Object.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Filters records using a predicate function, similar to Array.filter\n\t * @param {Function} fn - Predicate function to test each record (record, key, store)\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of records that pass the predicate test\n\t * @throws {Error} Throws error if fn is not a function\n\t * @example\n\t * const adults = store.filter(record => record.age >= 18);\n\t * const recent = store.filter(record => record.created > Date.now() - 86400000);\n\t */\n\tfilter (fn, raw = false) {\n\t\tif (typeof fn !== STRING_FUNCTION) {\n\t\t\tthrow new Error(STRING_INVALID_FUNCTION);\n\t\t}\n\t\tlet result = this.reduce((a, v) => {\n\t\t\tif (fn(v)) {\n\t\t\t\ta.push(v);\n\t\t\t}\n\n\t\t\treturn a;\n\t\t}, []);\n\t\tif (!raw) {\n\t\t\tresult = result.map(i => this.list(i));\n\n\t\t\tif (this.immutable) {\n\t\t\t\tresult = Object.freeze(result);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Executes a function for each record in the store, similar to Array.forEach\n\t * @param {Function} fn - Function to execute for each record (value, key)\n\t * @param {*} [ctx] - Context object to use as 'this' when executing the function\n\t * @returns {Haro} This instance for method chaining\n\t * @example\n\t * store.forEach((record, key) => {\n\t * console.log(`${key}: ${record.name}`);\n\t * });\n\t */\n\tforEach (fn, ctx = this) {\n\t\tthis.data.forEach((value, key) => {\n\t\t\tif (this.immutable) {\n\t\t\t\tvalue = this.clone(value);\n\t\t\t}\n\t\t\tfn.call(ctx, value, key);\n\t\t}, this);\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Creates a frozen array from the given arguments for immutable data handling\n\t * @param {...*} args - Arguments to freeze into an array\n\t * @returns {Array<*>} Frozen array containing frozen arguments\n\t * @example\n\t * const frozen = store.freeze(obj1, obj2, obj3);\n\t * // Returns Object.freeze([Object.freeze(obj1), Object.freeze(obj2), Object.freeze(obj3)])\n\t */\n\tfreeze (...args) {\n\t\treturn Object.freeze(args.map(i => Object.freeze(i)));\n\t}\n\n\t/**\n\t * Retrieves a record by its key\n\t * @param {string} key - Key of record to retrieve\n\t * @param {boolean} [raw=false] - Whether to return raw data (true) or processed/frozen data (false)\n\t * @returns {Object|null} The record if found, null if not found\n\t * @example\n\t * const user = store.get('user123');\n\t * const rawUser = store.get('user123', true);\n\t */\n\tget (key, raw = false) {\n\t\tlet result = this.data.get(key) ?? null;\n\t\tif (result !== null && !raw) {\n\t\t\tresult = this.list(result);\n\t\t\tif (this.immutable) {\n\t\t\t\tresult = Object.freeze(result);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Checks if a record with the specified key exists in the store\n\t * @param {string} key - Key to check for existence\n\t * @returns {boolean} True if record exists, false otherwise\n\t * @example\n\t * if (store.has('user123')) {\n\t * console.log('User exists');\n\t * }\n\t */\n\thas (key) {\n\t\treturn this.data.has(key);\n\t}\n\n\t/**\n\t * Generates index keys for composite indexes from data values\n\t * @param {string} [arg=STRING_EMPTY] - Composite index field names joined by delimiter\n\t * @param {string} [delimiter=STRING_PIPE] - Delimiter used in composite index\n\t * @param {Object} [data={}] - Data object to extract field values from\n\t * @returns {string[]} Array of generated index keys\n\t * @example\n\t * // For index 'name|department' with data {name: 'John', department: 'IT'}\n\t * const keys = store.indexKeys('name|department', '|', data);\n\t * // Returns ['John|IT']\n\t */\n\tindexKeys (arg = STRING_EMPTY, delimiter = STRING_PIPE, data = {}) {\n\t\tconst fields = arg.split(delimiter).sort(this.sortKeys);\n\t\tconst fieldsLen = fields.length;\n\t\tlet result = [\"\"];\n\t\tfor (let i = 0; i < fieldsLen; i++) {\n\t\t\tconst field = fields[i];\n\t\t\tconst values = Array.isArray(data[field]) ? data[field] : [data[field]];\n\t\t\tconst newResult = [];\n\t\t\tconst resultLen = result.length;\n\t\t\tconst valuesLen = values.length;\n\t\t\tfor (let j = 0; j < resultLen; j++) {\n\t\t\t\tfor (let k = 0; k < valuesLen; k++) {\n\t\t\t\t\tconst newKey = i === 0 ? values[k] : `${result[j]}${delimiter}${values[k]}`;\n\t\t\t\t\tnewResult.push(newKey);\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult = newResult;\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Returns an iterator of all keys in the store\n\t * @returns {Iterator} Iterator of record keys\n\t * @example\n\t * for (const key of store.keys()) {\n\t * console.log(key);\n\t * }\n\t */\n\tkeys () {\n\t\treturn this.data.keys();\n\t}\n\n\t/**\n\t * Returns a limited subset of records with offset support for pagination\n\t * @param {number} [offset=INT_0] - Number of records to skip from the beginning\n\t * @param {number} [max=INT_0] - Maximum number of records to return\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of records within the specified range\n\t * @example\n\t * const page1 = store.limit(0, 10); // First 10 records\n\t * const page2 = store.limit(10, 10); // Next 10 records\n\t */\n\tlimit (offset = INT_0, max = INT_0, raw = false) {\n\t\tlet result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw));\n\t\tif (!raw && this.immutable) {\n\t\t\tresult = Object.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Converts a record into a [key, value] pair array format\n\t * @param {Object} arg - Record object to convert to list format\n\t * @returns {Array<*>} Array containing [key, record] where key is extracted from record's key field\n\t * @example\n\t * const record = {id: 'user123', name: 'John', age: 30};\n\t * const pair = store.list(record); // ['user123', {id: 'user123', name: 'John', age: 30}]\n\t */\n\tlist (arg) {\n\t\tconst result = [arg[this.key], arg];\n\n\t\treturn this.immutable ? this.freeze(...result) : result;\n\t}\n\n\t/**\n\t * Transforms all records using a mapping function, similar to Array.map\n\t * @param {Function} fn - Function to transform each record (record, key)\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array<*>} Array of transformed results\n\t * @throws {Error} Throws error if fn is not a function\n\t * @example\n\t * const names = store.map(record => record.name);\n\t * const summaries = store.map(record => ({id: record.id, name: record.name}));\n\t */\n\tmap (fn, raw = false) {\n\t\tif (typeof fn !== STRING_FUNCTION) {\n\t\t\tthrow new Error(STRING_INVALID_FUNCTION);\n\t\t}\n\t\tlet result = [];\n\t\tthis.forEach((value, key) => result.push(fn(value, key)));\n\t\tif (!raw) {\n\t\t\tresult = result.map(i => this.list(i));\n\t\t\tif (this.immutable) {\n\t\t\t\tresult = Object.freeze(result);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Merges two values together with support for arrays and objects\n\t * @param {*} a - First value (target)\n\t * @param {*} b - Second value (source)\n\t * @param {boolean} [override=false] - Whether to override arrays instead of concatenating\n\t * @returns {*} Merged result\n\t * @example\n\t * const merged = store.merge({a: 1}, {b: 2}); // {a: 1, b: 2}\n\t * const arrays = store.merge([1, 2], [3, 4]); // [1, 2, 3, 4]\n\t */\n\tmerge (a, b, override = false) {\n\t\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\t\ta = override ? b : a.concat(b);\n\t\t} else if (typeof a === STRING_OBJECT && a !== null && typeof b === STRING_OBJECT && b !== null) {\n\t\t\tthis.each(Object.keys(b), i => {\n\t\t\t\ta[i] = this.merge(a[i], b[i], override);\n\t\t\t});\n\t\t} else {\n\t\t\ta = b;\n\t\t}\n\n\t\treturn a;\n\t}\n\n\t/**\n\t * Lifecycle hook executed after batch operations for custom postprocessing\n\t * @param {Array} arg - Result of batch operation\n\t * @param {string} [type=STRING_EMPTY] - Type of batch operation that was performed\n\t * @returns {Array} Modified result (override this method to implement custom logic)\n\t */\n\tonbatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\treturn arg;\n\t}\n\n\t/**\n\t * Lifecycle hook executed after clear operation for custom postprocessing\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t * @example\n\t * class MyStore extends Haro {\n\t * onclear() {\n\t * console.log('Store cleared');\n\t * }\n\t * }\n\t */\n\tonclear () {\n\t\t// Hook for custom logic after clear; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed after delete operation for custom postprocessing\n\t * @param {string} [key=STRING_EMPTY] - Key of deleted record\n\t * @param {boolean} [batch=false] - Whether this was part of a batch operation\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tondelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic after delete; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed after override operation for custom postprocessing\n\t * @param {string} [type=STRING_EMPTY] - Type of override operation that was performed\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tonoverride (type = STRING_EMPTY) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic after override; override in subclass if needed\n\t}\n\n\t/**\n\t * Lifecycle hook executed after set operation for custom postprocessing\n\t * @param {Object} [arg={}] - Record that was set\n\t * @param {boolean} [batch=false] - Whether this was part of a batch operation\n\t * @returns {void} Override this method in subclasses to implement custom logic\n\t */\n\tonset (arg = {}, batch = false) { // eslint-disable-line no-unused-vars\n\t\t// Hook for custom logic after set; override in subclass if needed\n\t}\n\n\t/**\n\t * Replaces all store data or indexes with new data for bulk operations\n\t * @param {Array} data - Data to replace with (format depends on type)\n\t * @param {string} [type=STRING_RECORDS] - Type of data: 'records' or 'indexes'\n\t * @returns {boolean} True if operation succeeded\n\t * @throws {Error} Throws error if type is invalid\n\t * @example\n\t * const records = [['key1', {name: 'John'}], ['key2', {name: 'Jane'}]];\n\t * store.override(records, 'records');\n\t */\n\toverride (data, type = STRING_RECORDS) {\n\t\tconst result = true;\n\t\tif (type === STRING_INDEXES) {\n\t\t\tthis.indexes = new Map(data.map(i => [i[0], new Map(i[1].map(ii => [ii[0], new Set(ii[1])]))]));\n\t\t} else if (type === STRING_RECORDS) {\n\t\t\tthis.indexes.clear();\n\t\t\tthis.data = new Map(data);\n\t\t} else {\n\t\t\tthrow new Error(STRING_INVALID_TYPE);\n\t\t}\n\t\tthis.onoverride(type);\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Reduces all records to a single value using a reducer function\n\t * @param {Function} fn - Reducer function (accumulator, value, key, store)\n\t * @param {*} [accumulator] - Initial accumulator value\n\t * @returns {*} Final reduced value\n\t * @example\n\t * const totalAge = store.reduce((sum, record) => sum + record.age, 0);\n\t * const names = store.reduce((acc, record) => acc.concat(record.name), []);\n\t */\n\treduce (fn, accumulator = []) {\n\t\tlet a = accumulator;\n\t\tthis.forEach((v, k) => {\n\t\t\ta = fn(a, v, k, this);\n\t\t}, this);\n\n\t\treturn a;\n\t}\n\n\t/**\n\t * Rebuilds indexes for specified fields or all fields for data consistency\n\t * @param {string|string[]} [index] - Specific index field(s) to rebuild, or all if not specified\n\t * @returns {Haro} This instance for method chaining\n\t * @example\n\t * store.reindex(); // Rebuild all indexes\n\t * store.reindex('name'); // Rebuild only name index\n\t * store.reindex(['name', 'email']); // Rebuild name and email indexes\n\t */\n\treindex (index) {\n\t\tconst indices = index ? [index] : this.index;\n\t\tif (index && this.index.includes(index) === false) {\n\t\t\tthis.index.push(index);\n\t\t}\n\t\tthis.each(indices, i => this.indexes.set(i, new Map()));\n\t\tthis.forEach((data, key) => this.each(indices, i => this.setIndex(key, data, i)));\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Searches for records containing a value across specified indexes\n\t * @param {*} value - Value to search for (string, function, or RegExp)\n\t * @param {string|string[]} [index] - Index(es) to search in, or all if not specified\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of matching records\n\t * @example\n\t * const results = store.search('john'); // Search all indexes\n\t * const nameResults = store.search('john', 'name'); // Search only name index\n\t * const regexResults = store.search(/^admin/, 'role'); // Regex search\n\t */\n\tsearch (value, index, raw = false) {\n\t\tconst result = new Set(); // Use Set for unique keys\n\t\tconst fn = typeof value === STRING_FUNCTION;\n\t\tconst rgex = value && typeof value.test === STRING_FUNCTION;\n\t\tif (!value) return this.immutable ? this.freeze() : [];\n\t\tconst indices = index ? Array.isArray(index) ? index : [index] : this.index;\n\t\tfor (const i of indices) {\n\t\t\tconst idx = this.indexes.get(i);\n\t\t\tif (idx) {\n\t\t\t\tfor (const [lkey, lset] of idx) {\n\t\t\t\t\tlet match = false;\n\n\t\t\t\t\tif (fn) {\n\t\t\t\t\t\tmatch = value(lkey, i);\n\t\t\t\t\t} else if (rgex) {\n\t\t\t\t\t\tmatch = value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmatch = lkey === value;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (match) {\n\t\t\t\t\t\tfor (const key of lset) {\n\t\t\t\t\t\t\tif (this.data.has(key)) {\n\t\t\t\t\t\t\t\tresult.add(key);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlet records = Array.from(result).map(key => this.get(key, raw));\n\t\tif (!raw && this.immutable) {\n\t\t\trecords = Object.freeze(records);\n\t\t}\n\n\t\treturn records;\n\t}\n\n\t/**\n\t * Sets or updates a record in the store with automatic indexing\n\t * @param {string|null} [key=null] - Key for the record, or null to use record's key field\n\t * @param {Object} [data={}] - Record data to set\n\t * @param {boolean} [batch=false] - Whether this is part of a batch operation\n\t * @param {boolean} [override=false] - Whether to override existing data instead of merging\n\t * @returns {Object} The stored record (frozen if immutable mode)\n\t * @example\n\t * const user = store.set(null, {name: 'John', age: 30}); // Auto-generate key\n\t * const updated = store.set('user123', {age: 31}); // Update existing record\n\t */\n\tset (key = null, data = {}, batch = false, override = false) {\n\t\tif (key === null) {\n\t\t\tkey = data[this.key] ?? this.uuid();\n\t\t}\n\t\tlet x = {...data, [this.key]: key};\n\t\tthis.beforeSet(key, x, batch, override);\n\t\tif (!this.data.has(key)) {\n\t\t\tif (this.versioning) {\n\t\t\t\tthis.versions.set(key, new Set());\n\t\t\t}\n\t\t} else {\n\t\t\tconst og = this.get(key, true);\n\t\t\tthis.deleteIndex(key, og);\n\t\t\tif (this.versioning) {\n\t\t\t\tthis.versions.get(key).add(Object.freeze(this.clone(og)));\n\t\t\t}\n\t\t\tif (!override) {\n\t\t\t\tx = this.merge(this.clone(og), x);\n\t\t\t}\n\t\t}\n\t\tthis.data.set(key, x);\n\t\tthis.setIndex(key, x, null);\n\t\tconst result = this.get(key);\n\t\tthis.onset(result, batch);\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Internal method to add entries to indexes for a record\n\t * @param {string} key - Key of record being indexed\n\t * @param {Object} data - Data of record being indexed\n\t * @param {string|null} indice - Specific index to update, or null for all\n\t * @returns {Haro} This instance for method chaining\n\t */\n\tsetIndex (key, data, indice) {\n\t\tthis.each(indice === null ? this.index : [indice], i => {\n\t\t\tlet idx = this.indexes.get(i);\n\t\t\tif (!idx) {\n\t\t\t\tidx = new Map();\n\t\t\t\tthis.indexes.set(i, idx);\n\t\t\t}\n\t\t\tconst fn = c => {\n\t\t\t\tif (!idx.has(c)) {\n\t\t\t\t\tidx.set(c, new Set());\n\t\t\t\t}\n\t\t\t\tidx.get(c).add(key);\n\t\t\t};\n\t\t\tif (i.includes(this.delimiter)) {\n\t\t\t\tthis.each(this.indexKeys(i, this.delimiter, data), fn);\n\t\t\t} else {\n\t\t\t\tthis.each(Array.isArray(data[i]) ? data[i] : [data[i]], fn);\n\t\t\t}\n\t\t});\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Sorts all records using a comparator function\n\t * @param {Function} fn - Comparator function for sorting (a, b) => number\n\t * @param {boolean} [frozen=false] - Whether to return frozen records\n\t * @returns {Array} Sorted array of records\n\t * @example\n\t * const sorted = store.sort((a, b) => a.age - b.age); // Sort by age\n\t * const names = store.sort((a, b) => a.name.localeCompare(b.name)); // Sort by name\n\t */\n\tsort (fn, frozen = false) {\n\t\tconst dataSize = this.data.size;\n\t\tlet result = this.limit(INT_0, dataSize, true).sort(fn);\n\t\tif (frozen) {\n\t\t\tresult = this.freeze(...result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Comparator function for sorting keys with type-aware comparison logic\n\t * @param {*} a - First value to compare\n\t * @param {*} b - Second value to compare\n\t * @returns {number} Negative number if a < b, positive if a > b, zero if equal\n\t * @example\n\t * const keys = ['name', 'age', 'email'];\n\t * keys.sort(store.sortKeys); // Alphabetical sort\n\t *\n\t * const mixed = [10, '5', 'abc', 3];\n\t * mixed.sort(store.sortKeys); // Type-aware sort: numbers first, then strings\n\t */\n\tsortKeys (a, b) {\n\t\t// Handle string comparison\n\t\tif (typeof a === STRING_STRING && typeof b === STRING_STRING) {\n\t\t\treturn a.localeCompare(b);\n\t\t}\n\t\t// Handle numeric comparison\n\t\tif (typeof a === STRING_NUMBER && typeof b === STRING_NUMBER) {\n\t\t\treturn a - b;\n\t\t}\n\n\t\t// Handle mixed types or other types by converting to string\n\n\t\treturn String(a).localeCompare(String(b));\n\t}\n\n\t/**\n\t * Sorts records by a specific indexed field in ascending order\n\t * @param {string} [index=STRING_EMPTY] - Index field name to sort by\n\t * @param {boolean} [raw=false] - Whether to return raw data without processing\n\t * @returns {Array} Array of records sorted by the specified field\n\t * @throws {Error} Throws error if index field is empty or invalid\n\t * @example\n\t * const byAge = store.sortBy('age');\n\t * const byName = store.sortBy('name');\n\t */\n\tsortBy (index = STRING_EMPTY, raw = false) {\n\t\tif (index === STRING_EMPTY) {\n\t\t\tthrow new Error(STRING_INVALID_FIELD);\n\t\t}\n\t\tlet result = [];\n\t\tconst keys = [];\n\t\tif (this.indexes.has(index) === false) {\n\t\t\tthis.reindex(index);\n\t\t}\n\t\tconst lindex = this.indexes.get(index);\n\t\tlindex.forEach((idx, key) => keys.push(key));\n\t\tthis.each(keys.sort(this.sortKeys), i => lindex.get(i).forEach(key => result.push(this.get(key, raw))));\n\t\tif (this.immutable) {\n\t\t\tresult = Object.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Converts all store data to a plain array of records\n\t * @returns {Array} Array containing all records in the store\n\t * @example\n\t * const allRecords = store.toArray();\n\t * console.log(`Store contains ${allRecords.length} records`);\n\t */\n\ttoArray () {\n\t\tconst result = Array.from(this.data.values());\n\t\tif (this.immutable) {\n\t\t\tthis.each(result, i => Object.freeze(i));\n\t\t\tObject.freeze(result);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Generates a RFC4122 v4 UUID for record identification\n\t * @returns {string} UUID string in standard format\n\t * @example\n\t * const id = store.uuid(); // \"f47ac10b-58cc-4372-a567-0e02b2c3d479\"\n\t */\n\tuuid () {\n\t\treturn uuid();\n\t}\n\n\t/**\n\t * Returns an iterator of all values in the store\n\t * @returns {Iterator} Iterator of record values\n\t * @example\n\t * for (const record of store.values()) {\n\t * console.log(record.name);\n\t * }\n\t */\n\tvalues () {\n\t\treturn this.data.values();\n\t}\n\n\t/**\n\t * Internal helper method for predicate matching with support for arrays and regex\n\t * @param {Object} record - Record to test against predicate\n\t * @param {Object} predicate - Predicate object with field-value pairs\n\t * @param {string} op - Operator for array matching ('||' for OR, '&&' for AND)\n\t * @returns {boolean} True if record matches predicate criteria\n\t */\n\tmatchesPredicate (record, predicate, op) {\n\t\tconst keys = Object.keys(predicate);\n\n\t\treturn keys.every(key => {\n\t\t\tconst pred = predicate[key];\n\t\t\tconst val = record[key];\n\t\t\tif (Array.isArray(pred)) {\n\t\t\t\tif (Array.isArray(val)) {\n\t\t\t\t\treturn op === STRING_DOUBLE_AND ? pred.every(p => val.includes(p)) : pred.some(p => val.includes(p));\n\t\t\t\t} else {\n\t\t\t\t\treturn op === STRING_DOUBLE_AND ? pred.every(p => val === p) : pred.some(p => val === p);\n\t\t\t\t}\n\t\t\t} else if (pred instanceof RegExp) {\n\t\t\t\tif (Array.isArray(val)) {\n\t\t\t\t\treturn op === STRING_DOUBLE_AND ? val.every(v => pred.test(v)) : val.some(v => pred.test(v));\n\t\t\t\t} else {\n\t\t\t\t\treturn pred.test(val);\n\t\t\t\t}\n\t\t\t} else if (Array.isArray(val)) {\n\t\t\t\treturn val.includes(pred);\n\t\t\t} else {\n\t\t\t\treturn val === pred;\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Advanced filtering with predicate logic supporting AND/OR operations on arrays\n\t * @param {Object} [predicate={}] - Object with field-value pairs for filtering\n\t * @param {string} [op=STRING_DOUBLE_PIPE] - Operator for array matching ('||' for OR, '&&' for AND)\n\t * @returns {Array} Array of records matching the predicate criteria\n\t * @example\n\t * // Find records with tags containing 'admin' OR 'user'\n\t * const users = store.where({tags: ['admin', 'user']}, '||');\n\t *\n\t * // Find records with ALL specified tags\n\t * const powerUsers = store.where({tags: ['admin', 'power']}, '&&');\n\t *\n\t * // Regex matching\n\t * const emails = store.where({email: /^admin@/});\n\t */\n\twhere (predicate = {}, op = STRING_DOUBLE_PIPE) {\n\t\tconst keys = this.index.filter(i => i in predicate);\n\t\tif (keys.length === 0) return [];\n\n\t\t// Try to use indexes for better performance\n\t\tconst indexedKeys = keys.filter(k => this.indexes.has(k));\n\t\tif (indexedKeys.length > 0) {\n\t\t\t// Use index-based filtering for better performance\n\t\t\tlet candidateKeys = new Set();\n\t\t\tlet first = true;\n\t\t\tfor (const key of indexedKeys) {\n\t\t\t\tconst pred = predicate[key];\n\t\t\t\tconst idx = this.indexes.get(key);\n\t\t\t\tconst matchingKeys = new Set();\n\t\t\t\tif (Array.isArray(pred)) {\n\t\t\t\t\tfor (const p of pred) {\n\t\t\t\t\t\tif (idx.has(p)) {\n\t\t\t\t\t\t\tfor (const k of idx.get(p)) {\n\t\t\t\t\t\t\t\tmatchingKeys.add(k);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (idx.has(pred)) {\n\t\t\t\t\tfor (const k of idx.get(pred)) {\n\t\t\t\t\t\tmatchingKeys.add(k);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (first) {\n\t\t\t\t\tcandidateKeys = matchingKeys;\n\t\t\t\t\tfirst = false;\n\t\t\t\t} else {\n\t\t\t\t\t// AND operation across different fields\n\t\t\t\t\tcandidateKeys = new Set([...candidateKeys].filter(k => matchingKeys.has(k)));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Filter candidates with full predicate logic\n\t\t\tconst results = [];\n\t\t\tfor (const key of candidateKeys) {\n\t\t\t\tconst record = this.get(key, true);\n\t\t\t\tif (this.matchesPredicate(record, predicate, op)) {\n\t\t\t\t\tresults.push(this.immutable ? this.get(key) : record);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn this.immutable ? this.freeze(...results) : results;\n\t\t}\n\n\t\t// Fallback to full scan if no indexes available\n\t\treturn this.filter(a => this.matchesPredicate(a, predicate, op));\n\t}\n}\n\n/**\n * Factory function to create a new Haro instance with optional initial data\n * @param {Array|null} [data=null] - Initial data to populate the store\n * @param {Object} [config={}] - Configuration object passed to Haro constructor\n * @returns {Haro} New Haro instance configured and optionally populated\n * @example\n * const store = haro([\n * {id: 1, name: 'John', age: 30},\n * {id: 2, name: 'Jane', age: 25}\n * ], {\n * index: ['name', 'age'],\n * versioning: true\n * });\n */\nexport function haro (data = null, config = {}) {\n\tconst obj = new Haro(config);\n\n\tif (Array.isArray(data)) {\n\t\tobj.batch(data, STRING_SET);\n\t}\n\n\treturn obj;\n}\n"],"names":["g","f","exports","module","require","define","amd","globalThis","self","lru","crypto","this","STRING_EMPTY","STRING_DOUBLE_AND","STRING_FUNCTION","STRING_OBJECT","STRING_RECORDS","STRING_STRING","STRING_NUMBER","STRING_INVALID_FUNCTION","Haro","constructor","delimiter","id","uuid","immutable","index","key","versioning","data","Map","Array","isArray","indexes","versions","Object","defineProperty","enumerable","get","from","keys","size","reindex","batch","args","type","fn","i","delete","set","onbatch","beforeBatch","map","arg","beforeClear","beforeDelete","beforeSet","override","clear","onclear","clone","structuredClone","has","Error","og","deleteIndex","ondelete","forEach","idx","values","includes","indexKeys","each","value","o","dump","result","entries","ii","arr","len","length","find","where","raw","sort","sortKeys","join","reduce","a","v","k","add","Set","freeze","filter","push","list","ctx","call","fields","split","fieldsLen","field","newResult","resultLen","valuesLen","j","newKey","limit","offset","max","registry","slice","merge","b","concat","onoverride","onset","accumulator","indices","setIndex","search","rgex","test","lkey","lset","match","records","x","indice","c","frozen","dataSize","localeCompare","String","sortBy","lindex","toArray","matchesPredicate","record","predicate","op","every","pred","val","p","some","RegExp","indexedKeys","candidateKeys","first","matchingKeys","results","haro","config","obj"],"mappings":"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"} \ No newline at end of file diff --git a/docs/CODE_STYLE_GUIDE.md b/docs/CODE_STYLE_GUIDE.md new file mode 100644 index 00000000..f1b49481 --- /dev/null +++ b/docs/CODE_STYLE_GUIDE.md @@ -0,0 +1,519 @@ +# Code Style Guide + +This document outlines the coding standards and conventions for the Haro project. Following these guidelines ensures consistent, maintainable, and readable code across the entire codebase. + +## Table of Contents + +1. [General Principles](#general-principles) +2. [JavaScript Language Guidelines](#javascript-language-guidelines) +3. [Naming Conventions](#naming-conventions) +4. [Code Structure](#code-structure) +5. [Documentation Standards](#documentation-standards) +6. [Testing Standards](#testing-standards) +7. [Error Handling](#error-handling) +8. [Performance Considerations](#performance-considerations) +9. [Security Guidelines](#security-guidelines) +10. [ESLint Configuration](#eslint-configuration) + +## General Principles + +### Code Quality +- Write code that is **readable**, **maintainable**, and **testable** +- Follow the **principle of least surprise** - code should behave as expected +- Use **meaningful names** for variables, functions, and classes +- Keep functions **small and focused** on a single responsibility +- Write **self-documenting code** with clear intent + +### Consistency +- Follow established patterns within the codebase +- Use consistent indentation and formatting +- Maintain uniform error handling patterns +- Apply naming conventions consistently + +## JavaScript Language Guidelines + +### ES6+ Features +Use modern JavaScript features appropriately: + +```javascript +// ✅ Good - Use const/let instead of var +const API_ENDPOINT = 'https://api.example.com'; +let userData = null; + +// ✅ Good - Use arrow functions for concise syntax +const processData = data => data.map(item => item.value); + +// ✅ Good - Use template literals +const message = `Processing ${count} items`; + +// ✅ Good - Use destructuring +const {name, age} = user; +const [first, second] = array; + +// ✅ Good - Use spread operator +const newArray = [...existingArray, newItem]; +``` + +### Variable Declarations +```javascript +// ✅ Good - Use const for immutable values +const MAX_RETRIES = 3; +const config = {timeout: 5000}; + +// ✅ Good - Use let for mutable values +let currentIndex = 0; +let isProcessing = false; + +// ❌ Bad - Avoid var +var oldStyleVariable = 'avoid this'; +``` + +### Function Declarations +```javascript +// ✅ Good - Use function declarations for named functions +function processRecord(record) { + return record.transform(); +} + +// ✅ Good - Use arrow functions for callbacks and short functions +const numbers = [1, 2, 3].map(n => n * 2); + +// ✅ Good - Use consistent spacing +function calculateTotal (items) { + return items.reduce((sum, item) => sum + item.price, 0); +} +``` + +## Naming Conventions + +### Variables and Functions +- Use **camelCase** for all variable and function names +- Use **descriptive names** that clearly indicate purpose + +```javascript +// ✅ Good +const userAccountBalance = 1000; +const getCurrentUser = () => {...}; +const processPaymentRequest = (request) => {...}; + +// ❌ Bad +const uab = 1000; +const getUsr = () => {...}; +const proc = (req) => {...}; +``` + +### Constants +- Use **UPPER_SNAKE_CASE** for constants +- Group related constants together + +```javascript +// ✅ Good +const MAX_RETRY_ATTEMPTS = 3; +const DEFAULT_TIMEOUT = 5000; +const ERROR_MESSAGES = { + INVALID_INPUT: 'Invalid input provided', + NETWORK_ERROR: 'Network connection failed' +}; +``` + +### Classes +- Use **PascalCase** for class names +- Use **camelCase** for methods and properties + +```javascript +// ✅ Good +class DataProcessor { + constructor(options) { + this.processingOptions = options; + } + + processData(data) { + return this.transformData(data); + } +} +``` + +### Files and Modules +- Use **kebab-case** for file names +- Use **camelCase** for module exports + +```javascript +// File: data-processor.js +export class DataProcessor {...} +export const processData = () => {...}; +``` + +## Code Structure + +### Indentation and Formatting +- Use **tabs** for indentation (not spaces) +- Use **consistent brace style** (1TBS with single-line allowance) +- Keep lines reasonably short (aim for readability) + +```javascript +// ✅ Good - Consistent indentation and brace style +function processItems(items) { + if (items.length === 0) { + return []; + } + + return items.map(item => { + if (item.isValid) { + return item.process(); + } + + return item.getDefault(); + }); +} +``` + +### Import/Export Organization +```javascript +// ✅ Good - Group imports logically +import {randomUUID} from 'crypto'; +import {promises as fs} from 'fs'; + +import { + STRING_EMPTY, + STRING_INVALID_TYPE, + MAX_RETRIES +} from './constants.js'; + +// Export at the end of file +export {DataProcessor}; +export {processData}; +``` + +### Object and Array Formatting +```javascript +// ✅ Good - Multi-line formatting for complex objects +const config = { + server: { + host: 'localhost', + port: 3000 + }, + database: { + url: 'mongodb://localhost:27017', + options: { + useUnifiedTopology: true + } + } +}; + +// ✅ Good - Single line for simple objects +const point = {x: 10, y: 20}; +``` + +## Documentation Standards + +### JSDoc Comments +Use **JSDoc standard** for all functions and classes: + +```javascript +/** + * Processes user data and returns formatted results + * @param {Object} userData - Raw user data to process + * @param {string} userData.name - User's full name + * @param {number} userData.age - User's age + * @param {Object} [options={}] - Processing options + * @param {boolean} [options.validate=true] - Whether to validate input + * @returns {Object} Processed user data + * @throws {Error} Throws error if validation fails + * @example + * const result = processUserData({name: 'John', age: 30}); + * // Returns: {name: 'John', age: 30, processed: true} + */ +function processUserData(userData, options = {}) { + // Implementation +} +``` + +### Class Documentation +```javascript +/** + * Manages data storage and retrieval with indexing capabilities + * @class + * @example + * const store = new DataStore({ + * index: ['name', 'email'], + * immutable: true + * }); + */ +class DataStore { + /** + * Creates a new DataStore instance + * @param {Object} config - Configuration options + * @param {string[]} [config.index=[]] - Fields to index + * @param {boolean} [config.immutable=false] - Enable immutable mode + */ + constructor(config = {}) { + // Implementation + } +} +``` + +### Code Comments +```javascript +// ✅ Good - Explain WHY, not WHAT +function calculateDiscount(price, customerType) { + // Premium customers get 15% discount to encourage loyalty + const discountRate = customerType === 'premium' ? 0.15 : 0.05; + + return price * (1 - discountRate); +} + +// ✅ Good - Document complex algorithms +function complexCalculation(data) { + // Using Boyer-Moore algorithm for efficient string searching + // This approach reduces time complexity from O(n*m) to O(n+m) + return algorithmImplementation(data); +} +``` + +## Testing Standards + +### Unit Tests +- Place unit tests in `tests/unit/` directory +- Use **node-assert** for assertions +- Run tests with **Mocha** +- Follow **AAA pattern** (Arrange, Act, Assert) + +```javascript +// tests/unit/data-processor.test.js +import assert from 'node:assert'; +import {DataProcessor} from '../../src/data-processor.js'; + +describe('DataProcessor', () => { + describe('processData', () => { + it('should transform valid data correctly', () => { + // Arrange + const processor = new DataProcessor(); + const inputData = [{id: 1, name: 'test'}]; + + // Act + const result = processor.processData(inputData); + + // Assert + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].processed, true); + }); + + it('should throw error for invalid input', () => { + // Arrange + const processor = new DataProcessor(); + + // Act & Assert + assert.throws(() => { + processor.processData(null); + }, {message: 'Invalid input provided'}); + }); + }); +}); +``` + +### Integration Tests +- Place integration tests in `tests/integration/` directory +- Test complete workflows and system interactions +- Use realistic data and scenarios + +```javascript +// tests/integration/store-operations.test.js +import assert from 'node:assert'; +import {Haro} from '../../src/haro.js'; + +describe('Store Operations Integration', () => { + it('should handle complete CRUD workflow', () => { + // Arrange + const store = new Haro({ + index: ['name', 'email'], + versioning: true + }); + + // Act - Create + const user = store.set(null, {name: 'John', email: 'john@example.com'}); + + // Assert - Create + assert.ok(user); + assert.strictEqual(user.name, 'John'); + + // Act - Read + const found = store.find({name: 'John'}); + + // Assert - Read + assert.strictEqual(found.length, 1); + assert.strictEqual(found[0].email, 'john@example.com'); + }); +}); +``` + +## Error Handling + +### Error Types +```javascript +// ✅ Good - Use descriptive error messages +if (!data) { + throw new Error('Data parameter is required'); +} + +if (typeof data !== 'object') { + throw new Error('Data must be an object'); +} + +// ✅ Good - Use specific error types when appropriate +class ValidationError extends Error { + constructor(message, field) { + super(message); + this.name = 'ValidationError'; + this.field = field; + } +} +``` + +### Error Handling Patterns +```javascript +// ✅ Good - Handle errors gracefully +function processData(data) { + try { + validateData(data); + return transformData(data); + } catch (error) { + if (error instanceof ValidationError) { + // Handle validation errors specifically + return {error: error.message, field: error.field}; + } + // Re-throw unexpected errors + throw error; + } +} + +// ✅ Good - Use consistent error responses +function apiHandler(request) { + try { + return {success: true, data: processRequest(request)}; + } catch (error) { + return {success: false, error: error.message}; + } +} +``` + +## Performance Considerations + +### Efficient Data Structures +```javascript +// ✅ Good - Use Map for frequent lookups +const userCache = new Map(); + +// ✅ Good - Use Set for unique collections +const processedIds = new Set(); + +// ✅ Good - Use appropriate array methods +const activeUsers = users.filter(user => user.isActive); +const userNames = users.map(user => user.name); +``` + +### Memory Management +```javascript +// ✅ Good - Clean up references +function processLargeDataSet(data) { + const processor = new DataProcessor(); + const result = processor.process(data); + + // Clean up large objects + processor.cleanup(); + + return result; +} + +// ✅ Good - Use streaming for large data +function processLargeFile(filePath) { + const stream = fs.createReadStream(filePath); + return stream.pipe(new DataProcessor()); +} +``` + +## Security Guidelines + +### Input Validation +```javascript +// ✅ Good - Validate all inputs +function setUserData(userData) { + if (!userData || typeof userData !== 'object') { + throw new Error('Invalid user data'); + } + + if (!userData.email || !isValidEmail(userData.email)) { + throw new Error('Valid email is required'); + } + + // Sanitize input + const sanitizedData = sanitizeUserInput(userData); + return sanitizedData; +} +``` + +### Safe Object Access +```javascript +// ✅ Good - Use optional chaining +const userName = user?.profile?.name || 'Anonymous'; + +// ✅ Good - Validate object structure +function processUserProfile(profile) { + if (!profile || typeof profile !== 'object') { + throw new Error('Invalid profile object'); + } + + const {name, email} = profile; + if (!name || !email) { + throw new Error('Profile must contain name and email'); + } + + return {name, email}; +} +``` + +## ESLint Configuration + +The project uses ESLint for code quality enforcement. Key rules include: + +- **Indentation**: Tabs with consistent variable declaration alignment +- **Quotes**: Double quotes with escape avoidance +- **Semicolons**: Required +- **Brace Style**: 1TBS with single-line allowance +- **Comma Style**: Trailing commas not allowed +- **Space Requirements**: Consistent spacing around operators and keywords +- **No Unused Variables**: All variables must be used +- **Consistent Returns**: Functions should have consistent return patterns + +### Running ESLint +```bash +# Check all files +npm run lint + +# Fix auto-fixable issues +npm run lint:fix +``` + +## Best Practices Summary + +1. **Use meaningful names** for variables, functions, and classes +2. **Write comprehensive JSDoc comments** for all public APIs +3. **Keep functions small** and focused on single responsibility +4. **Handle errors gracefully** with appropriate error types +5. **Write thorough tests** for all functionality +6. **Use modern JavaScript features** appropriately +7. **Follow consistent formatting** with tab indentation +8. **Validate all inputs** and sanitize user data +9. **Use appropriate data structures** for performance +10. **Clean up resources** to prevent memory leaks + +## Tools and Automation + +- **ESLint**: Code quality and style enforcement +- **Mocha**: Test runner for unit and integration tests +- **Node Assert**: Assertion library for testing +- **Rollup**: Module bundler for distribution +- **Husky**: Git hooks for pre-commit checks + +--- + +*This style guide is a living document. As the project evolves, these guidelines should be updated to reflect new patterns and best practices.* \ No newline at end of file diff --git a/docs/TECHNICAL_DOCUMENTATION.md b/docs/TECHNICAL_DOCUMENTATION.md new file mode 100644 index 00000000..18969746 --- /dev/null +++ b/docs/TECHNICAL_DOCUMENTATION.md @@ -0,0 +1,830 @@ +# Haro Technical Documentation + +## Overview + +Haro is a modern, immutable DataStore designed for high-performance data operations with advanced indexing, versioning, and batch processing capabilities. It provides a Map-like interface optimized for complex querying scenarios in modern JavaScript applications. + +## Table of Contents + +- [Architecture](#architecture) +- [Core Components](#core-components) +- [Data Flow](#data-flow) +- [Indexing System](#indexing-system) +- [Operations](#operations) +- [Configuration](#configuration) +- [Performance Characteristics](#performance-characteristics) +- [Usage Patterns](#usage-patterns) +- [2025 Application Examples](#2025-application-examples) +- [API Reference](#api-reference) +- [Best Practices](#best-practices) + +## Architecture + +Haro's architecture is built around five core components that work together to provide efficient data management: + +```mermaid +graph TB + A["🏗️ Haro Instance"] --> B["📊 Data Store
(Map)"] + A --> C["🔍 Index System
(Map of Maps)"] + A --> D["📚 Version Store
(Map of Sets)"] + A --> E["⚙️ Configuration
(Options)"] + + B --> F["🔑 Primary Keys"] + B --> G["📝 Record Data"] + + C --> H["📇 Field Indexes"] + C --> I["🔗 Composite Indexes"] + + D --> J["📜 Version History"] + D --> K["🔄 Change Tracking"] + + E --> L["🏷️ Key Field"] + E --> M["🔒 Immutable Mode"] + E --> N["📊 Index Fields"] + + classDef dataStore fill:#0066CC,stroke:#004499,stroke-width:2px,color:#fff + classDef indexSystem fill:#008000,stroke:#006600,stroke-width:2px,color:#fff + classDef versionStore fill:#FF8C00,stroke:#CC7000,stroke-width:2px,color:#fff + classDef config fill:#6600CC,stroke:#440088,stroke-width:2px,color:#fff + classDef detail fill:#666666,stroke:#444444,stroke-width:1px,color:#fff + + class A,B dataStore + class C,H,I indexSystem + class D,J,K versionStore + class E,L,M,N config + class F,G detail +``` + +## Core Components + +### Data Store (Map) +- **Purpose**: Primary storage for all records +- **Structure**: `Map` +- **Features**: Fast O(1) key-based access, automatic key generation + +### Index System (Map of Maps) +- **Purpose**: Accelerated queries and searches +- **Structure**: `Map>>` +- **Features**: Multi-field indexing, composite keys, automatic maintenance + +### Version Store (Map of Sets) +- **Purpose**: Track historical versions of records +- **Structure**: `Map>` +- **Features**: Immutable version snapshots, configurable retention + +### Configuration +- **Purpose**: Store instance settings and behavior +- **Options**: Immutable mode, versioning, custom delimiters, key fields + +## Data Flow + +### Record Creation Flow + +```mermaid +sequenceDiagram + participant Client + participant Haro + participant DataStore + participant IndexSystem + participant VersionStore + + Client->>+Haro: set(key, data) + + Note over Haro: Validate and prepare data + Haro->>Haro: beforeSet(key, data) + + alt Key exists + Haro->>+DataStore: get(key) + DataStore-->>-Haro: existing record + Haro->>+IndexSystem: deleteIndex(key, oldData) + IndexSystem-->>-Haro: indexes updated + + opt Versioning enabled + Haro->>+VersionStore: add version + VersionStore-->>-Haro: version stored + end + + Haro->>Haro: merge(oldData, newData) + end + + Haro->>+DataStore: set(key, processedData) + DataStore-->>-Haro: record stored + + Haro->>+IndexSystem: setIndex(key, data) + IndexSystem-->>-Haro: indexes updated + + Haro->>Haro: onset(record) + + Haro-->>-Client: processed record +``` + +### Query Processing Flow + +```mermaid +flowchart TD + A["🔍 Query Request"] --> B{"Index Available?"} + + B -->|Yes| C["📇 Index Lookup"] + B -->|No| D["🔄 Full Scan"] + + C --> E["🔑 Extract Keys"] + D --> F["🔍 Filter Records"] + + E --> G["📊 Fetch Records"] + F --> G + + G --> H{"Immutable Mode?"} + + H -->|Yes| I["🔒 Freeze Results"] + H -->|No| J["✅ Return Results"] + + I --> J + + classDef query fill:#0066CC,stroke:#004499,stroke-width:2px,color:#fff + classDef index fill:#008000,stroke:#006600,stroke-width:2px,color:#fff + classDef scan fill:#FF8C00,stroke:#CC7000,stroke-width:2px,color:#fff + classDef result fill:#6600CC,stroke:#440088,stroke-width:2px,color:#fff + + class A,B query + class C,E index + class D,F scan + class G,H,I,J result +``` + +## Indexing System + +Haro's indexing system provides O(1) lookup performance for indexed fields: + +### Index Types + +```mermaid +graph LR + A["🏷️ Index Types"] --> B["📊 Single Field
name → users"] + A --> C["🔗 Composite
name|dept → users"] + A --> D["📚 Array Field
tags[*] → users"] + + B --> E["🔍 Direct Lookup
O(1) complexity"] + C --> F["🔍 Multi-key Lookup
O(k) complexity"] + D --> G["🔍 Array Search
O(m) complexity"] + + classDef indexType fill:#0066CC,stroke:#004499,stroke-width:2px,color:#fff + classDef performance fill:#008000,stroke:#006600,stroke-width:2px,color:#fff + + class A,B,C,D indexType + class E,F,G performance +``` + +### Index Maintenance + +```mermaid +stateDiagram-v2 + [*] --> IndexCreation + IndexCreation --> IndexReady + + IndexReady --> RecordAdded: set() + RecordAdded --> UpdateIndex: Add keys + UpdateIndex --> IndexReady + + IndexReady --> RecordUpdated: set() existing + RecordUpdated --> RemoveOldKeys: Delete old + RemoveOldKeys --> AddNewKeys: Add new + AddNewKeys --> IndexReady + + IndexReady --> RecordDeleted: delete() + RecordDeleted --> RemoveKeys: Clean up + RemoveKeys --> IndexReady + + IndexReady --> Reindex: reindex() + Reindex --> RebuildComplete: Full rebuild + RebuildComplete --> IndexReady +``` + +## Operations + +### CRUD Operations Performance + +| Operation | Time Complexity | Space Complexity | Notes | +|-----------|----------------|------------------|--------| +| **Create** | O(1) + O(i) | O(1) | i = number of indexes | +| **Read** | O(1) | O(1) | Direct key access | +| **Update** | O(1) + O(i) | O(1) | Index maintenance | +| **Delete** | O(1) + O(i) | O(1) | Cleanup indexes | +| **Find** | O(1) | O(r) | r = result set size | +| **Search** | O(n) | O(r) | Full scan fallback | +| **Batch** | O(n) + O(ni) | O(n) | n = batch size | + +### Batch Operations + +```mermaid +graph TD + A["📦 Batch Request"] --> B["🔄 beforeBatch()"] + B --> C["📊 Process Items"] + + C --> D["🔗 Parallel Processing"] + D --> E1["⚡ Item 1"] + D --> E2["⚡ Item 2"] + D --> E3["⚡ Item N"] + + E1 --> F["📝 Individual Operation"] + E2 --> F + E3 --> F + + F --> G["📊 Collect Results"] + G --> H["🔄 onbatch()"] + H --> I["✅ Return Results"] + + classDef batch fill:#0066CC,stroke:#004499,stroke-width:2px,color:#fff + classDef process fill:#008000,stroke:#006600,stroke-width:2px,color:#fff + classDef parallel fill:#FF8C00,stroke:#CC7000,stroke-width:2px,color:#fff + + class A,B,H,I batch + class C,F,G process + class D,E1,E2,E3 parallel +``` + +## Configuration + +### Initialization Options + +```javascript +const store = new Haro({ + // Primary key field (default: 'id') + key: 'userId', + + // Index configuration + index: ['name', 'email', 'department', 'name|department'], + + // Immutable mode - returns frozen objects + immutable: true, + + // Version tracking + versioning: true, + + // Composite key delimiter + delimiter: '|', + + // Instance identifier + id: 'user-store-1' +}); +``` + +### Runtime Configuration + +```mermaid +graph TD + A["⚙️ Configuration"] --> B["🔑 Key Field"] + A --> C["📇 Index Fields"] + A --> D["🔒 Immutable Mode"] + A --> E["📚 Versioning"] + A --> F["🔗 Delimiter"] + + B --> G["🎯 Primary Key Selection"] + C --> H["⚡ Query Optimization"] + D --> I["🛡️ Data Protection"] + E --> J["📜 Change Tracking"] + F --> K["🔗 Composite Keys"] + + classDef config fill:#6600CC,stroke:#440088,stroke-width:2px,color:#fff + classDef feature fill:#0066CC,stroke:#004499,stroke-width:2px,color:#fff + + class A,B,C,D,E,F config + class G,H,I,J,K feature +``` + +## Performance Characteristics + +### Memory Usage + +```mermaid +pie title Memory Distribution + "Record Data" : 60 + "Index Structures" : 25 + "Version History" : 10 + "Metadata" : 5 +``` + +### Query Performance + +```mermaid +xychart-beta + title "Query Performance by Data Size" + x-axis [1K, 10K, 100K, 1M, 10M] + y-axis "Response Time (ms)" 0 --> 100 + line "Indexed Query" [0.1, 0.15, 0.2, 0.3, 0.5] + line "Full Scan" [1, 10, 100, 1000, 10000] +``` + +## Usage Patterns + +### Real-time Data Management + +```javascript +// Configure for real-time updates +const realtimeStore = new Haro({ + index: ['userId', 'sessionId', 'timestamp'], + versioning: true, + immutable: true +}); + +// Handle real-time events +function handleUserEvent(event) { + const record = realtimeStore.set(null, { + userId: event.userId, + sessionId: event.sessionId, + timestamp: Date.now(), + action: event.action, + data: event.payload + }); + + // Broadcast to connected clients + broadcastUpdate(record); +} +``` + +### Caching Layer + +```javascript +// Cache configuration +const cache = new Haro({ + key: 'cacheKey', + index: ['category', 'expiry'], + immutable: false +}); + +// Cache with TTL +function setCache(key, data, ttl = 3600000) { + return cache.set(key, { + cacheKey: key, + data: data, + expiry: Date.now() + ttl, + category: 'api-response' + }); +} + +// Cleanup expired entries +function cleanupCache() { + const now = Date.now(); + const expired = cache.filter(record => record.expiry < now); + expired.forEach(record => cache.delete(record.cacheKey)); +} +``` + +### State Management + +```javascript +// Application state store +const appState = new Haro({ + key: 'stateKey', + index: ['component', 'namespace'], + versioning: true, + immutable: true +}); + +// State management functions +const stateManager = { + setState(component, namespace, data) { + return appState.set(`${component}:${namespace}`, { + stateKey: `${component}:${namespace}`, + component, + namespace, + timestamp: Date.now(), + data + }); + }, + + getState(component, namespace) { + return appState.get(`${component}:${namespace}`); + }, + + getComponentState(component) { + return appState.find({ component }); + } +}; +``` + +## 2025 Application Examples + +### Edge Computing Data Store + +```javascript +// Edge computing node data management +const edgeStore = new Haro({ + key: 'deviceId', + index: ['location', 'deviceType', 'status', 'location|deviceType'], + versioning: true, + immutable: true +}); + +// Handle IoT device data +class EdgeDataManager { + constructor() { + this.store = edgeStore; + this.syncQueue = []; + } + + async registerDevice(device) { + const record = this.store.set(null, { + deviceId: device.id, + location: device.coordinates, + deviceType: device.type, + status: 'online', + lastSeen: Date.now(), + capabilities: device.capabilities, + metadata: device.metadata + }); + + // Queue for cloud sync + this.queueSync('device-register', record); + return record; + } + + getDevicesByLocation(lat, lon, radius) { + return this.store.filter(device => { + const distance = this.calculateDistance( + lat, lon, + device.location.lat, device.location.lon + ); + return distance <= radius; + }); + } + + async syncToCloud() { + const batch = this.syncQueue.splice(0, 100); + await this.cloudSync.batch(batch); + } +} +``` + +### Real-time Collaborative Platform + +```javascript +// Collaborative document editing +const collaborativeStore = new Haro({ + key: 'operationId', + index: ['documentId', 'userId', 'timestamp', 'documentId|timestamp'], + versioning: true, + immutable: true +}); + +class CollaborativeEditor { + constructor(documentId) { + this.documentId = documentId; + this.store = collaborativeStore; + this.operationalTransform = new OperationalTransform(); + } + + applyOperation(operation) { + // Store operation with conflict resolution + const record = this.store.set(null, { + operationId: this.generateOperationId(), + documentId: this.documentId, + userId: operation.userId, + timestamp: Date.now(), + type: operation.type, + position: operation.position, + content: operation.content, + transformedAgainst: operation.transformedAgainst || [] + }); + + // Get concurrent operations for transformation + const concurrentOps = this.getConcurrentOperations( + operation.timestamp, + operation.userId + ); + + // Apply operational transformation + const transformedOp = this.operationalTransform.transform( + operation, + concurrentOps + ); + + // Broadcast to connected clients + this.broadcastOperation(transformedOp); + + return record; + } + + getConcurrentOperations(timestamp, excludeUserId) { + return this.store.find({ + documentId: this.documentId + }).filter(op => + op.timestamp >= timestamp && + op.userId !== excludeUserId + ); + } +} +``` + +### AI/ML Feature Store + +```javascript +// Machine learning feature store +const featureStore = new Haro({ + key: 'featureId', + index: ['entityId', 'featureType', 'version', 'entityId|featureType'], + versioning: true, + immutable: true +}); + +class MLFeatureStore { + constructor() { + this.store = featureStore; + this.computeEngine = new FeatureComputeEngine(); + } + + async storeFeatures(entityId, features) { + const batch = Object.entries(features).map(([featureType, value]) => ({ + featureId: `${entityId}:${featureType}:${Date.now()}`, + entityId, + featureType, + value, + version: this.getNextVersion(entityId, featureType), + timestamp: Date.now(), + computedBy: 'feature-pipeline-v2', + metadata: { + pipeline: 'realtime', + source: 'user-behavior' + } + })); + + return this.store.batch(batch, 'set'); + } + + getFeatureVector(entityId, featureTypes, version = 'latest') { + const features = {}; + + for (const featureType of featureTypes) { + const featureHistory = this.store.find({ + entityId, + featureType + }); + + const feature = version === 'latest' + ? featureHistory.reduce((latest, current) => + current.version > latest.version ? current : latest + ) + : featureHistory.find(f => f.version === version); + + if (feature) { + features[featureType] = feature.value; + } + } + + return features; + } + + async computeOnlineFeatures(entityId, context) { + const onlineFeatures = await this.computeEngine.compute(entityId, context); + return this.storeFeatures(entityId, onlineFeatures); + } +} +``` + +### Serverless Function State + +```javascript +// Serverless function state management +const functionState = new Haro({ + key: 'executionId', + index: ['functionName', 'status', 'timestamp', 'functionName|status'], + versioning: false, + immutable: true +}); + +class ServerlessStateManager { + constructor() { + this.store = functionState; + this.ttl = 15 * 60 * 1000; // 15 minutes + } + + async trackExecution(functionName, executionId, input) { + return this.store.set(executionId, { + executionId, + functionName, + status: 'running', + timestamp: Date.now(), + input: this.sanitizeInput(input), + startTime: Date.now(), + region: process.env.AWS_REGION, + memoryUsage: process.memoryUsage(), + coldStart: this.isColdStart() + }); + } + + async completeExecution(executionId, result, error = null) { + const execution = this.store.get(executionId); + if (!execution) throw new Error('Execution not found'); + + return this.store.set(executionId, { + ...execution, + status: error ? 'error' : 'completed', + endTime: Date.now(), + duration: Date.now() - execution.startTime, + result: error ? null : result, + error: error ? error.message : null, + finalMemoryUsage: process.memoryUsage() + }); + } + + getExecutionMetrics(functionName, timeRange = 3600000) { + const since = Date.now() - timeRange; + const executions = this.store.find({ functionName }) + .filter(exec => exec.timestamp >= since); + + return { + totalExecutions: executions.length, + successRate: executions.filter(e => e.status === 'completed').length / executions.length, + avgDuration: executions.reduce((sum, e) => sum + (e.duration || 0), 0) / executions.length, + coldStarts: executions.filter(e => e.coldStart).length, + errorRate: executions.filter(e => e.status === 'error').length / executions.length + }; + } +} +``` + +### Progressive Web App Offline Store + +```javascript +// PWA offline-first data store +const offlineStore = new Haro({ + key: 'id', + index: ['type', 'syncStatus', 'lastModified', 'type|syncStatus'], + versioning: true, + immutable: false +}); + +class PWAOfflineManager { + constructor() { + this.store = offlineStore; + this.syncQueue = []; + this.isOnline = navigator.onLine; + + // Listen for online/offline events + window.addEventListener('online', () => this.handleOnline()); + window.addEventListener('offline', () => this.handleOffline()); + } + + async saveOffline(type, data) { + const record = this.store.set(null, { + id: data.id || this.generateId(), + type, + data, + syncStatus: 'pending', + lastModified: Date.now(), + createdOffline: !this.isOnline + }); + + // Queue for sync when online + if (!this.isOnline) { + this.queueForSync(record); + } else { + await this.syncRecord(record); + } + + return record; + } + + async handleOnline() { + this.isOnline = true; + + // Sync all pending records + const pendingRecords = this.store.find({ syncStatus: 'pending' }); + + for (const record of pendingRecords) { + try { + await this.syncRecord(record); + } catch (error) { + console.error('Sync failed for record:', record.id, error); + // Mark as failed for retry + this.store.set(record.id, { + ...record, + syncStatus: 'failed', + lastSyncError: error.message + }); + } + } + } + + async syncRecord(record) { + try { + const response = await fetch('/api/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(record) + }); + + if (response.ok) { + this.store.set(record.id, { + ...record, + syncStatus: 'synced', + lastSynced: Date.now() + }); + } + } catch (error) { + throw new Error(`Sync failed: ${error.message}`); + } + } + + getOfflineData(type) { + return this.store.find({ type }); + } +} +``` + +## API Reference + +### Constructor + +```javascript +new Haro(config) +``` + +**Parameters:** +- `config` (Object): Configuration options + - `key` (string): Primary key field name + - `index` (string[]): Fields to index + - `immutable` (boolean): Enable immutable mode + - `versioning` (boolean): Enable version tracking + - `delimiter` (string): Composite key delimiter + +### Core Methods + +| Method | Description | Time Complexity | +|--------|-------------|----------------| +| `set(key, data)` | Create or update record | O(1) + O(i) | +| `get(key)` | Retrieve record by key | O(1) | +| `delete(key)` | Remove record | O(1) + O(i) | +| `find(criteria)` | Query with indexes | O(1) to O(n) | +| `search(value, index)` | Search across indexes | O(n) | +| `batch(records, type)` | Bulk operations | O(n) + O(ni) | +| `clear()` | Remove all records | O(n) | + +### Query Methods + +| Method | Description | Use Case | +|--------|-------------|----------| +| `filter(predicate)` | Filter with function | Complex logic | +| `where(criteria, op)` | Advanced filtering | Multi-condition queries | +| `sortBy(field)` | Sort by indexed field | Ordered results | +| `limit(offset, max)` | Pagination | Large datasets | +| `map(transform)` | Transform records | Data projection | + +## Best Practices + +### Index Design + +```javascript +// ✅ Good - Index frequently queried fields +const userStore = new Haro({ + index: ['email', 'department', 'status', 'department|status'] +}); + +// ❌ Bad - Too many indexes impact write performance +const overIndexed = new Haro({ + index: ['field1', 'field2', 'field3', 'field4', 'field5', 'field6'] +}); +``` + +### Memory Management + +```javascript +// ✅ Good - Use versioning selectively +const auditStore = new Haro({ + versioning: true, // Only for audit trails + immutable: true +}); + +// ✅ Good - Batch operations for bulk updates +const records = [...largeDataset]; +store.batch(records, 'set'); + +// ❌ Bad - Individual operations for bulk data +largeDataset.forEach(record => store.set(null, record)); +``` + +### Query Optimization + +```javascript +// ✅ Good - Use indexed queries +const results = store.find({ status: 'active', department: 'engineering' }); + +// ❌ Bad - Full scan with filter +const results = store.filter(r => r.status === 'active' && r.department === 'engineering'); +``` + +### Error Handling + +```javascript +// ✅ Good - Graceful error handling +try { + const record = store.set(null, userData); + return { success: true, data: record }; +} catch (error) { + console.error('Store operation failed:', error); + return { success: false, error: error.message }; +} +``` + +--- + +*This technical documentation provides comprehensive coverage of Haro's capabilities and implementation patterns for modern applications. For additional support, refer to the [Code Style Guide](CODE_STYLE_GUIDE.md) and project examples.* \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 192527d3..6509734b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,7 +4,7 @@ import pluginJs from "@eslint/js"; export default [ // Mocha environment for test files { - files: ["test/**/*.js"], + files: ["tests/**/*.js"], languageOptions: { globals: { ...globals.mocha @@ -16,7 +16,8 @@ export default [ globals: { ...globals.node, it: true, - describe: true + describe: true, + crypto: true }, parserOptions: { ecmaVersion: 2022 diff --git a/package-lock.json b/package-lock.json index 067448a2..c79f596b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,303 +1,44 @@ { "name": "haro", - "version": "15.2.6", + "version": "16.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "haro", - "version": "15.2.6", + "version": "16.0.0", "license": "BSD-3-Clause", "devDependencies": { - "@eslint/js": "^9.6.0", + "@eslint/js": "^9.31.0", "@rollup/plugin-terser": "^0.4.4", "auto-changelog": "^2.5.0", - "eslint": "^9.27.0", - "globals": "^16.1.0", + "c8": "^10.1.3", + "eslint": "^9.31.0", + "globals": "^16.3.0", "husky": "^9.1.7", - "mocha": "^11.3.0", - "nyc": "^17.1.0", - "precise": "^4.0.3", - "rollup": "^4.40.2", - "typescript": "^5.8.3" + "mocha": "^11.7.1", + "rollup": "^4.45.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=17.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -316,6 +57,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -328,6 +70,7 @@ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -356,9 +99,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -428,13 +171,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -446,6 +189,7 @@ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } @@ -455,6 +199,7 @@ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" @@ -468,6 +213,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -481,6 +227,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -490,9 +237,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -508,6 +255,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -520,287 +268,25 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -808,40 +294,35 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -852,6 +333,7 @@ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -862,6 +344,7 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "dev": true, + "license": "MIT", "dependencies": { "serialize-javascript": "^6.0.1", "smob": "^1.0.0", @@ -880,9 +363,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz", - "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz", + "integrity": "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==", "cpu": [ "arm" ], @@ -893,9 +376,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz", - "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.0.tgz", + "integrity": "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==", "cpu": [ "arm64" ], @@ -906,9 +389,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz", - "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.0.tgz", + "integrity": "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==", "cpu": [ "arm64" ], @@ -919,9 +402,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz", - "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.0.tgz", + "integrity": "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==", "cpu": [ "x64" ], @@ -932,9 +415,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz", - "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.0.tgz", + "integrity": "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==", "cpu": [ "arm64" ], @@ -945,9 +428,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz", - "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.0.tgz", + "integrity": "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==", "cpu": [ "x64" ], @@ -958,9 +441,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz", - "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.0.tgz", + "integrity": "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==", "cpu": [ "arm" ], @@ -971,9 +454,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz", - "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.0.tgz", + "integrity": "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==", "cpu": [ "arm" ], @@ -984,9 +467,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz", - "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.0.tgz", + "integrity": "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==", "cpu": [ "arm64" ], @@ -997,9 +480,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz", - "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.0.tgz", + "integrity": "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==", "cpu": [ "arm64" ], @@ -1010,9 +493,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz", - "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.0.tgz", + "integrity": "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==", "cpu": [ "loong64" ], @@ -1023,9 +506,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz", - "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.0.tgz", + "integrity": "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==", "cpu": [ "ppc64" ], @@ -1036,9 +519,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz", - "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.0.tgz", + "integrity": "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==", "cpu": [ "riscv64" ], @@ -1049,9 +532,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz", - "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.0.tgz", + "integrity": "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==", "cpu": [ "riscv64" ], @@ -1062,9 +545,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz", - "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.0.tgz", + "integrity": "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==", "cpu": [ "s390x" ], @@ -1075,9 +558,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz", - "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.0.tgz", + "integrity": "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==", "cpu": [ "x64" ], @@ -1088,9 +571,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz", - "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.0.tgz", + "integrity": "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==", "cpu": [ "x64" ], @@ -1101,9 +584,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz", - "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.0.tgz", + "integrity": "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==", "cpu": [ "arm64" ], @@ -1114,9 +597,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz", - "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.0.tgz", + "integrity": "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==", "cpu": [ "ia32" ], @@ -1127,9 +610,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz", - "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.0.tgz", + "integrity": "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==", "cpu": [ "x64" ], @@ -1146,6 +629,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1176,19 +666,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1207,12 +684,16 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -1220,6 +701,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1230,35 +712,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", - "dev": true, - "dependencies": { - "default-require-extensions": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/auto-changelog": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.5.0.tgz", "integrity": "sha512-UTnLjT7I9U2U/xkCUH5buDlp8C7g0SGChfib+iDrJkamcj5kaMqNKHNfbKJw1kthJUq8sUo3i3q2S6FzO/l/wA==", "dev": true, + "license": "MIT", "dependencies": { "commander": "^7.2.0", "handlebars": "^4.7.7", @@ -1274,88 +740,70 @@ "node": ">=8.3" } }, - "node_modules/auto-changelog/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/caching-transform": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", "dev": true, + "license": "ISC", "dependencies": { - "hasha": "^5.0.0", - "make-dir": "^3.0.0", - "package-hash": "^4.0.0", - "write-file-atomic": "^3.0.0" + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } } }, "node_modules/callsites": { @@ -1373,6 +821,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1380,31 +829,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001683", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001683.tgz", - "integrity": "sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1432,15 +862,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1456,6 +877,51 @@ "node": ">=12" } }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -1479,6 +945,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1490,37 +957,39 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1531,9 +1000,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1553,6 +1022,7 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1564,22 +1034,8 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/default-require-extensions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", - "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", "dev": true, - "dependencies": { - "strip-bom": "^4.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "node_modules/diff": { "version": "7.0.0", @@ -1595,31 +1051,22 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.5.64", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.64.tgz", - "integrity": "sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1629,6 +1076,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1758,24 +1206,12 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -1801,6 +1237,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1810,6 +1247,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -1832,13 +1270,15 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -1846,28 +1286,12 @@ "node": ">=16.0.0" } }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1884,6 +1308,7 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, + "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } @@ -1893,6 +1318,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -1902,18 +1328,20 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" }, "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, + "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -1923,50 +1351,13 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fromentries": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", - "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1975,38 +1366,22 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -2027,6 +1402,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -2035,10 +1411,11 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2048,6 +1425,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2070,17 +1448,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -2102,31 +1475,17 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", - "dev": true, - "dependencies": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, + "license": "MIT", "bin": { "he": "bin/he" } @@ -2135,13 +1494,15 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, + "license": "MIT", "bin": { "husky": "bin.js" }, @@ -2167,6 +1528,7 @@ "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", "dev": true, + "license": "MIT", "dependencies": { "import-from": "^3.0.0" }, @@ -2191,21 +1553,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/import-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", "dev": true, + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -2213,46 +1566,32 @@ "node": ">=8" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/import-from/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "license": "MIT", + "engines": { + "node": ">=0.8.19" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2262,6 +1601,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2271,6 +1611,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2283,33 +1624,17 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2317,71 +1642,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", - "dev": true, - "dependencies": { - "append-transform": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-processinfo": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", - "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", - "dev": true, - "dependencies": { - "archy": "^1.0.0", - "cross-spawn": "^7.0.3", - "istanbul-lib-coverage": "^3.2.0", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^8.3.2" - }, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -2391,6 +1664,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -2400,40 +1674,12 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -2447,6 +1693,7 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -2457,17 +1704,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2475,23 +1717,12 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -2504,25 +1735,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } + "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -2532,6 +1753,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -2545,6 +1767,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -2555,23 +1778,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -2584,43 +1803,34 @@ } }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } + "license": "ISC" }, "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { - "semver": "^6.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2633,6 +1843,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2642,6 +1853,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -2683,9 +1895,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2695,295 +1907,75 @@ "node_modules/mocha/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-preload": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", - "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", - "dev": true, - "dependencies": { - "process-on-spawn": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true - }, - "node_modules/nyc": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", - "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", - "dev": true, - "dependencies": { - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "caching-transform": "^4.0.0", - "convert-source-map": "^1.7.0", - "decamelize": "^1.2.0", - "find-cache-dir": "^3.2.0", - "find-up": "^4.1.0", - "foreground-child": "^3.3.0", - "get-package-type": "^0.1.0", - "glob": "^7.1.6", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-hook": "^3.0.0", - "istanbul-lib-instrument": "^6.0.2", - "istanbul-lib-processinfo": "^2.0.2", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "make-dir": "^3.0.0", - "node-preload": "^0.2.1", - "p-map": "^3.0.0", - "process-on-spawn": "^1.0.0", - "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", - "yargs": "^15.0.2" - }, - "bin": { - "nyc": "bin/nyc.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/nyc/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/nyc/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nyc/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/nyc/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "p-try": "^2.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=6" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/nyc/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/nyc/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/nyc/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" }, - "node_modules/nyc/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/nyc/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, + "license": "MIT", "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/optionator": { @@ -2991,6 +1983,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -3008,6 +2001,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -3023,6 +2017,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -3033,47 +2028,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { "version": "1.0.1", @@ -3093,6 +2053,7 @@ "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.3.tgz", "integrity": "sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==", "dev": true, + "license": "MIT", "bin": { "parse-github-url": "cli.js" }, @@ -3105,24 +2066,17 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3132,6 +2086,7 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -3143,112 +2098,23 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/precise": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/precise/-/precise-4.0.3.tgz", - "integrity": "sha512-UDy4UGrYbeRXskC5Mobm4jgUUDLgEuvPmYzkM5usrWMpsSBwe3N06T/JeTolmqOqvYs7P60ZS0yrm7IKBCyW1A==", - "dev": true, - "engines": { - "node": ">= 10.7.0" - } + "license": "ISC" }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, - "node_modules/process-on-spawn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", - "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", - "dev": true, - "dependencies": { - "fromentries": "^1.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3264,6 +2130,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -3282,83 +2149,30 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", - "dev": true, - "dependencies": { - "es6-error": "^4.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, + "license": "MIT", "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=4" } }, "node_modules/rollup": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz", - "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz", + "integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==", "dev": true, "dependencies": { "@types/estree": "1.0.8" @@ -3371,26 +2185,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.44.2", - "@rollup/rollup-android-arm64": "4.44.2", - "@rollup/rollup-darwin-arm64": "4.44.2", - "@rollup/rollup-darwin-x64": "4.44.2", - "@rollup/rollup-freebsd-arm64": "4.44.2", - "@rollup/rollup-freebsd-x64": "4.44.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", - "@rollup/rollup-linux-arm-musleabihf": "4.44.2", - "@rollup/rollup-linux-arm64-gnu": "4.44.2", - "@rollup/rollup-linux-arm64-musl": "4.44.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-musl": "4.44.2", - "@rollup/rollup-linux-s390x-gnu": "4.44.2", - "@rollup/rollup-linux-x64-gnu": "4.44.2", - "@rollup/rollup-linux-x64-musl": "4.44.2", - "@rollup/rollup-win32-arm64-msvc": "4.44.2", - "@rollup/rollup-win32-ia32-msvc": "4.44.2", - "@rollup/rollup-win32-x64-msvc": "4.44.2", + "@rollup/rollup-android-arm-eabi": "4.45.0", + "@rollup/rollup-android-arm64": "4.45.0", + "@rollup/rollup-darwin-arm64": "4.45.0", + "@rollup/rollup-darwin-x64": "4.45.0", + "@rollup/rollup-freebsd-arm64": "4.45.0", + "@rollup/rollup-freebsd-x64": "4.45.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", + "@rollup/rollup-linux-arm-musleabihf": "4.45.0", + "@rollup/rollup-linux-arm64-gnu": "4.45.0", + "@rollup/rollup-linux-arm64-musl": "4.45.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-musl": "4.45.0", + "@rollup/rollup-linux-s390x-gnu": "4.45.0", + "@rollup/rollup-linux-x64-gnu": "4.45.0", + "@rollup/rollup-linux-x64-musl": "4.45.0", + "@rollup/rollup-win32-arm64-msvc": "4.45.0", + "@rollup/rollup-win32-ia32-msvc": "4.45.0", + "@rollup/rollup-win32-x64-msvc": "4.45.0", "fsevents": "~2.3.2" } }, @@ -3412,13 +2226,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3431,21 +2247,17 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -3458,27 +2270,37 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/smob": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -3488,66 +2310,99 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "node_modules/spawn-wrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { - "foreground-child": "^2.0.0", - "is-windows": "^1.0.2", - "make-dir": "^3.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/spawn-wrap/node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - }, + "license": "MIT", "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3555,11 +2410,12 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3569,6 +2425,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -3581,6 +2438,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3589,13 +2447,14 @@ } }, "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -3606,36 +2465,49 @@ "node": ">=10" } }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3645,13 +2517,15 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -3659,43 +2533,12 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, + "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -3704,36 +2547,6 @@ "node": ">=0.8.0" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3744,26 +2557,34 @@ "punycode": "^2.1.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, - "bin": { - "uuid": "dist/bin/uuid" + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" } }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, + "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -3774,6 +2595,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -3784,17 +2606,12 @@ "node": ">= 8" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3803,12 +2620,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/workerpool": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", - "integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", + "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", "dev": true, "license": "Apache-2.0" }, @@ -3817,6 +2635,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -3829,84 +2648,81 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/y18n": { @@ -3919,12 +2735,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -3959,6 +2769,7 @@ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, + "license": "MIT", "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", @@ -3969,11 +2780,57 @@ "node": ">=10" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 424bb2e0..3cea4e29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "haro", - "version": "15.2.6", + "version": "16.0.0", "description": "Haro is a modern immutable DataStore", "type": "module", "types": "types/haro.d.ts", @@ -14,17 +14,16 @@ "files": [ "dist/haro.cjs", "dist/haro.js", - "types/haro.d.ts" + "types" ], "scripts": { - "build": "npm run lint && npm run rollup && npm run mocha", - "benchmark": "node benchmark/benchmark.js", + "benchmark": "node benchmarks/index.js", + "build": "npm run lint && npm run rollup", "changelog": "auto-changelog -p", - "lint": "eslint --fix *.js src/*.js test/*.js", - "mocha": "nyc mocha test/*.js", + "lint": "eslint --fix *.js benchmarks/*.js src/*.js tests/**/*.js", + "mocha": "c8 mocha tests/**/*.js", "rollup": "rollup --config", "test": "npm run lint && npm run mocha", - "types": "npx -p typescript tsc src/haro.js --declaration --allowJs --emitDeclarationOnly --outDir types", "prepare": "husky" }, "repository": { @@ -47,19 +46,17 @@ "homepage": "https://github.com/avoidwork/haro", "engineStrict": true, "engines": { - "node": ">=12.0.0" + "node": ">=17.0.0" }, "devDependencies": { - "@eslint/js": "^9.6.0", + "@eslint/js": "^9.31.0", "@rollup/plugin-terser": "^0.4.4", "auto-changelog": "^2.5.0", - "eslint": "^9.27.0", - "globals": "^16.1.0", + "c8": "^10.1.3", + "eslint": "^9.31.0", + "globals": "^16.3.0", "husky": "^9.1.7", - "mocha": "^11.3.0", - "nyc": "^17.1.0", - "precise": "^4.0.3", - "rollup": "^4.40.2", - "typescript": "^5.8.3" + "mocha": "^11.7.1", + "rollup": "^4.45.0" } } diff --git a/src/constants.js b/src/constants.js index 0c06db2b..39370fd8 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,25 +1,28 @@ +// String constants - Single characters and symbols export const STRING_COMMA = ","; export const STRING_EMPTY = ""; export const STRING_PIPE = "|"; export const STRING_DOUBLE_PIPE = "||"; -export const STRING_A = "a"; -export const STRING_B = "b"; +export const STRING_DOUBLE_AND = "&&"; + +// String constants - Operation and type names +export const STRING_ID = "id"; export const STRING_DEL = "del"; export const STRING_FUNCTION = "function"; export const STRING_INDEXES = "indexes"; -export const STRING_INVALID_FIELD = "Invalid field"; -export const STRING_INVALID_FUNCTION = "Invalid function"; -export const STRING_INVALID_TYPE = "Invalid type"; export const STRING_OBJECT = "object"; -export const STRING_RECORD_NOT_FOUND = "Record not found"; export const STRING_RECORDS = "records"; export const STRING_REGISTRY = "registry"; export const STRING_SET = "set"; export const STRING_SIZE = "size"; +export const STRING_STRING = "string"; +export const STRING_NUMBER = "number"; + +// String constants - Error messages +export const STRING_INVALID_FIELD = "Invalid field"; +export const STRING_INVALID_FUNCTION = "Invalid function"; +export const STRING_INVALID_TYPE = "Invalid type"; +export const STRING_RECORD_NOT_FOUND = "Record not found"; + +// Integer constants export const INT_0 = 0; -export const INT_1 = 1; -export const INT_3 = 3; -export const INT_4 = 4; -export const INT_8 = 8; -export const INT_9 = 9; -export const INT_16 = 16; diff --git a/src/haro.js b/src/haro.js index 703445b6..6ffde936 100644 --- a/src/haro.js +++ b/src/haro.js @@ -1,34 +1,68 @@ -import {uuid} from "./uuid.js"; +import {randomUUID as uuid} from "crypto"; import { INT_0, STRING_COMMA, - STRING_DEL, + STRING_DEL, STRING_DOUBLE_AND, STRING_DOUBLE_PIPE, STRING_EMPTY, STRING_FUNCTION, + STRING_ID, STRING_INDEXES, STRING_INVALID_FIELD, STRING_INVALID_FUNCTION, - STRING_INVALID_TYPE, + STRING_INVALID_TYPE, STRING_NUMBER, STRING_OBJECT, STRING_PIPE, STRING_RECORD_NOT_FOUND, STRING_RECORDS, STRING_REGISTRY, STRING_SET, - STRING_SIZE + STRING_SIZE, STRING_STRING } from "./constants.js"; +/** + * Haro is a modern immutable DataStore for collections of records with indexing, + * versioning, and batch operations support. It provides a Map-like interface + * with advanced querying capabilities through indexes. + * @class + * @example + * const store = new Haro({ + * index: ['name', 'age'], + * key: 'id', + * versioning: true + * }); + * + * store.set(null, {name: 'John', age: 30}); + * const results = store.find({name: 'John'}); + */ export class Haro { - constructor ({delimiter = STRING_PIPE, id = this.uuid(), index = [], key = "id", versioning = false} = {}) { + /** + * Creates a new Haro instance with specified configuration + * @param {Object} [config={}] - Configuration object for the store + * @param {string} [config.delimiter=STRING_PIPE] - Delimiter for composite indexes (default: '|') + * @param {string} [config.id] - Unique identifier for this instance (auto-generated if not provided) + * @param {boolean} [config.immutable=false] - Return frozen/immutable objects for data safety + * @param {string[]} [config.index=[]] - Array of field names to create indexes for + * @param {string} [config.key=STRING_ID] - Primary key field name used for record identification + * @param {boolean} [config.versioning=false] - Enable versioning to track record changes + * @constructor + * @example + * const store = new Haro({ + * index: ['name', 'email', 'name|department'], + * key: 'userId', + * versioning: true, + * immutable: true + * }); + */ + constructor ({delimiter = STRING_PIPE, id = this.uuid(), immutable = false, index = [], key = STRING_ID, versioning = false} = {}) { this.data = new Map(); this.delimiter = delimiter; this.id = id; + this.immutable = immutable; this.index = Array.isArray(index) ? [...index] : []; this.indexes = new Map(); this.key = key; this.versions = new Map(); this.versioning = versioning; - Object.defineProperty(this, STRING_REGISTRY, { enumerable: true, get: () => Array.from(this.data.keys()) @@ -41,28 +75,78 @@ export class Haro { return this.reindex(); } + /** + * Performs batch operations on multiple records for efficient bulk processing + * @param {Array} args - Array of records to process + * @param {string} [type=STRING_SET] - Type of operation: 'set' for upsert, 'del' for delete + * @returns {Array} Array of results from the batch operation + * @throws {Error} Throws error if individual operations fail during batch processing + * @example + * const results = store.batch([ + * {id: 1, name: 'John'}, + * {id: 2, name: 'Jane'} + * ], 'set'); + */ batch (args, type = STRING_SET) { - const fn = type === STRING_DEL ? i => this.del(i, true) : i => this.set(null, i, true, true); + const fn = type === STRING_DEL ? i => this.delete(i, true) : i => this.set(null, i, true, true); return this.onbatch(this.beforeBatch(args, type).map(fn), type); } + /** + * Lifecycle hook executed before batch operations for custom preprocessing + * @param {Array} arg - Arguments passed to batch operation + * @param {string} [type=STRING_EMPTY] - Type of batch operation ('set' or 'del') + * @returns {Array} The arguments array (possibly modified) to be processed + */ beforeBatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars + // Hook for custom logic before batch; override in subclass if needed return arg; } + /** + * Lifecycle hook executed before clear operation for custom preprocessing + * @returns {void} Override this method in subclasses to implement custom logic + * @example + * class MyStore extends Haro { + * beforeClear() { + * this.backup = this.toArray(); + * } + * } + */ beforeClear () { // Hook for custom logic before clear; override in subclass if needed } - beforeDelete (key = STRING_EMPTY, batch = false) { - return [key, batch]; - } - - beforeSet (key = STRING_EMPTY, batch = false) { - return [key, batch]; - } - + /** + * Lifecycle hook executed before delete operation for custom preprocessing + * @param {string} [key=STRING_EMPTY] - Key of record to delete + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + beforeDelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic before delete; override in subclass if needed + } + + /** + * Lifecycle hook executed before set operation for custom preprocessing + * @param {string} [key=STRING_EMPTY] - Key of record to set + * @param {Object} [data={}] - Record data being set + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @param {boolean} [override=false] - Whether to override existing data + * @returns {void} Override this method in subclasses to implement custom logic + */ + beforeSet (key = STRING_EMPTY, data = {}, batch = false, override = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic before set; override in subclass if needed + } + + /** + * Removes all records, indexes, and versions from the store + * @returns {Haro} This instance for method chaining + * @example + * store.clear(); + * console.log(store.size); // 0 + */ clear () { this.beforeClear(); this.data.clear(); @@ -73,17 +157,36 @@ export class Haro { return this; } + /** + * Creates a deep clone of the given value, handling objects, arrays, and primitives + * @param {*} arg - Value to clone (any type) + * @returns {*} Deep clone of the argument + * @example + * const original = {name: 'John', tags: ['user', 'admin']}; + * const cloned = store.clone(original); + * cloned.tags.push('new'); // original.tags is unchanged + */ clone (arg) { - return JSON.parse(JSON.stringify(arg)); - } - - del (key = STRING_EMPTY, batch = false) { + return structuredClone(arg); + } + + /** + * Deletes a record from the store and removes it from all indexes + * @param {string} [key=STRING_EMPTY] - Key of record to delete + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @returns {void} + * @throws {Error} Throws error if record with the specified key is not found + * @example + * store.delete('user123'); + * // Throws error if 'user123' doesn't exist + */ + delete (key = STRING_EMPTY, batch = false) { if (!this.data.has(key)) { throw new Error(STRING_RECORD_NOT_FOUND); } const og = this.get(key, true); this.beforeDelete(key, batch); - this.delIndex(this.index, this.indexes, this.delimiter, key, og); + this.deleteIndex(key, og); this.data.delete(key); this.ondelete(key, batch); if (this.versioning) { @@ -91,12 +194,18 @@ export class Haro { } } - delIndex (index, indexes, delimiter, key, data) { - index.forEach(i => { - const idx = indexes.get(i); + /** + * Internal method to remove entries from indexes for a deleted record + * @param {string} key - Key of record being deleted + * @param {Object} data - Data of record being deleted + * @returns {Haro} This instance for method chaining + */ + deleteIndex (key, data) { + this.index.forEach(i => { + const idx = this.indexes.get(i); if (!idx) return; - const values = i.includes(delimiter) ? - this.indexKeys(i, delimiter, data) : + const values = i.includes(this.delimiter) ? + this.indexKeys(i, this.delimiter, data) : Array.isArray(data[i]) ? data[i] : [data[i]]; this.each(values, value => { if (idx.has(value)) { @@ -108,11 +217,20 @@ export class Haro { } }); }); + + return this; } + /** + * Exports complete store data or indexes for persistence or debugging + * @param {string} [type=STRING_RECORDS] - Type of data to export: 'records' or 'indexes' + * @returns {Array} Array of [key, value] pairs for records, or serialized index structure + * @example + * const records = store.dump('records'); + * const indexes = store.dump('indexes'); + */ dump (type = STRING_RECORDS) { let result; - if (type === STRING_RECORDS) { result = Array.from(this.entries()); } else { @@ -130,20 +248,46 @@ export class Haro { return result; } + /** + * Utility method to iterate over an array with a callback function + * @param {Array<*>} [arr=[]] - Array to iterate over + * @param {Function} fn - Function to call for each element (element, index) + * @returns {Array<*>} The original array for method chaining + * @example + * store.each([1, 2, 3], (item, index) => console.log(item, index)); + */ each (arr = [], fn) { - for (const [idx, value] of arr.entries()) { - fn(value, idx); + const len = arr.length; + for (let i = 0; i < len; i++) { + fn(arr[i], i); } return arr; } + /** + * Returns an iterator of [key, value] pairs for each record in the store + * @returns {Iterator>} Iterator of [key, value] pairs + * @example + * for (const [key, value] of store.entries()) { + * console.log(key, value); + * } + */ entries () { return this.data.entries(); } + /** + * Finds records matching the specified criteria using indexes for optimal performance + * @param {Object} [where={}] - Object with field-value pairs to match against + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of matching records (frozen if immutable mode) + * @example + * const users = store.find({department: 'engineering', active: true}); + * const admins = store.find({role: 'admin'}); + */ find (where = {}, raw = false) { - const key = Object.keys(where).sort((a, b) => a.localeCompare(b)).join(this.delimiter); + const key = Object.keys(where).sort(this.sortKeys).join(this.delimiter); const index = this.indexes.get(key) ?? new Map(); let result = []; if (index.size > 0) { @@ -156,82 +300,230 @@ export class Haro { return a; }, new Set())).map(i => this.get(i, raw)); } + if (!raw && this.immutable) { + result = Object.freeze(result); + } - return raw ? result : this.list(...result); + return result; } + /** + * Filters records using a predicate function, similar to Array.filter + * @param {Function} fn - Predicate function to test each record (record, key, store) + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records that pass the predicate test + * @throws {Error} Throws error if fn is not a function + * @example + * const adults = store.filter(record => record.age >= 18); + * const recent = store.filter(record => record.created > Date.now() - 86400000); + */ filter (fn, raw = false) { if (typeof fn !== STRING_FUNCTION) { throw new Error(STRING_INVALID_FUNCTION); } - const x = raw ? (k, v) => v : (k, v) => Object.freeze([k, Object.freeze(v)]); - const result = this.reduce((a, v, k, ctx) => { - if (fn.call(ctx, v)) { - a.push(x(k, v)); + let result = this.reduce((a, v) => { + if (fn(v)) { + a.push(v); } return a; }, []); + if (!raw) { + result = result.map(i => this.list(i)); - return raw ? result : Object.freeze(result); + if (this.immutable) { + result = Object.freeze(result); + } + } + + return result; } - forEach (fn, ctx) { - this.data.forEach((value, key) => fn(this.clone(value), this.clone(key)), ctx ?? this.data); + /** + * Executes a function for each record in the store, similar to Array.forEach + * @param {Function} fn - Function to execute for each record (value, key) + * @param {*} [ctx] - Context object to use as 'this' when executing the function + * @returns {Haro} This instance for method chaining + * @example + * store.forEach((record, key) => { + * console.log(`${key}: ${record.name}`); + * }); + */ + forEach (fn, ctx = this) { + this.data.forEach((value, key) => { + if (this.immutable) { + value = this.clone(value); + } + fn.call(ctx, value, key); + }, this); return this; } + /** + * Creates a frozen array from the given arguments for immutable data handling + * @param {...*} args - Arguments to freeze into an array + * @returns {Array<*>} Frozen array containing frozen arguments + * @example + * const frozen = store.freeze(obj1, obj2, obj3); + * // Returns Object.freeze([Object.freeze(obj1), Object.freeze(obj2), Object.freeze(obj3)]) + */ + freeze (...args) { + return Object.freeze(args.map(i => Object.freeze(i))); + } + + /** + * Retrieves a record by its key + * @param {string} key - Key of record to retrieve + * @param {boolean} [raw=false] - Whether to return raw data (true) or processed/frozen data (false) + * @returns {Object|null} The record if found, null if not found + * @example + * const user = store.get('user123'); + * const rawUser = store.get('user123', true); + */ get (key, raw = false) { - const result = this.clone(this.data.get(key) ?? null); + let result = this.data.get(key) ?? null; + if (result !== null && !raw) { + result = this.list(result); + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : this.list(key, result); + return result; } + /** + * Checks if a record with the specified key exists in the store + * @param {string} key - Key to check for existence + * @returns {boolean} True if record exists, false otherwise + * @example + * if (store.has('user123')) { + * console.log('User exists'); + * } + */ has (key) { return this.data.has(key); } + /** + * Generates index keys for composite indexes from data values + * @param {string} [arg=STRING_EMPTY] - Composite index field names joined by delimiter + * @param {string} [delimiter=STRING_PIPE] - Delimiter used in composite index + * @param {Object} [data={}] - Data object to extract field values from + * @returns {string[]} Array of generated index keys + * @example + * // For index 'name|department' with data {name: 'John', department: 'IT'} + * const keys = store.indexKeys('name|department', '|', data); + * // Returns ['John|IT'] + */ indexKeys (arg = STRING_EMPTY, delimiter = STRING_PIPE, data = {}) { - return arg.split(delimiter).reduce((a, li, lidx) => { - const result = []; - - (Array.isArray(data[li]) ? data[li] : [data[li]]).forEach(lli => lidx === INT_0 ? result.push(lli) : a.forEach(x => result.push(`${x}${delimiter}${lli}`))); + const fields = arg.split(delimiter).sort(this.sortKeys); + const fieldsLen = fields.length; + let result = [""]; + for (let i = 0; i < fieldsLen; i++) { + const field = fields[i]; + const values = Array.isArray(data[field]) ? data[field] : [data[field]]; + const newResult = []; + const resultLen = result.length; + const valuesLen = values.length; + for (let j = 0; j < resultLen; j++) { + for (let k = 0; k < valuesLen; k++) { + const newKey = i === 0 ? values[k] : `${result[j]}${delimiter}${values[k]}`; + newResult.push(newKey); + } + } + result = newResult; + } - return result; - }, []); + return result; } + /** + * Returns an iterator of all keys in the store + * @returns {Iterator} Iterator of record keys + * @example + * for (const key of store.keys()) { + * console.log(key); + * } + */ keys () { return this.data.keys(); } + /** + * Returns a limited subset of records with offset support for pagination + * @param {number} [offset=INT_0] - Number of records to skip from the beginning + * @param {number} [max=INT_0] - Maximum number of records to return + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records within the specified range + * @example + * const page1 = store.limit(0, 10); // First 10 records + * const page2 = store.limit(10, 10); // Next 10 records + */ limit (offset = INT_0, max = INT_0, raw = false) { - const result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw)); - - return raw ? result : this.list(...result); - } + let result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw)); + if (!raw && this.immutable) { + result = Object.freeze(result); + } - list (...args) { - return Object.freeze(args.map(i => Object.freeze(i))); + return result; } + /** + * Converts a record into a [key, value] pair array format + * @param {Object} arg - Record object to convert to list format + * @returns {Array<*>} Array containing [key, record] where key is extracted from record's key field + * @example + * const record = {id: 'user123', name: 'John', age: 30}; + * const pair = store.list(record); // ['user123', {id: 'user123', name: 'John', age: 30}] + */ + list (arg) { + const result = [arg[this.key], arg]; + + return this.immutable ? this.freeze(...result) : result; + } + + /** + * Transforms all records using a mapping function, similar to Array.map + * @param {Function} fn - Function to transform each record (record, key) + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array<*>} Array of transformed results + * @throws {Error} Throws error if fn is not a function + * @example + * const names = store.map(record => record.name); + * const summaries = store.map(record => ({id: record.id, name: record.name})); + */ map (fn, raw = false) { if (typeof fn !== STRING_FUNCTION) { throw new Error(STRING_INVALID_FUNCTION); } - - const result = []; - + let result = []; this.forEach((value, key) => result.push(fn(value, key))); + if (!raw) { + result = result.map(i => this.list(i)); + if (this.immutable) { + result = Object.freeze(result); + } + } - return raw ? result : this.list(...result); + return result; } + /** + * Merges two values together with support for arrays and objects + * @param {*} a - First value (target) + * @param {*} b - Second value (source) + * @param {boolean} [override=false] - Whether to override arrays instead of concatenating + * @returns {*} Merged result + * @example + * const merged = store.merge({a: 1}, {b: 2}); // {a: 1, b: 2} + * const arrays = store.merge([1, 2], [3, 4]); // [1, 2, 3, 4] + */ merge (a, b, override = false) { if (Array.isArray(a) && Array.isArray(b)) { a = override ? b : a.concat(b); - } else if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) { + } else if (typeof a === STRING_OBJECT && a !== null && typeof b === STRING_OBJECT && b !== null) { this.each(Object.keys(b), i => { a[i] = this.merge(a[i], b[i], override); }); @@ -242,29 +534,71 @@ export class Haro { return a; } + /** + * Lifecycle hook executed after batch operations for custom postprocessing + * @param {Array} arg - Result of batch operation + * @param {string} [type=STRING_EMPTY] - Type of batch operation that was performed + * @returns {Array} Modified result (override this method to implement custom logic) + */ onbatch (arg, type = STRING_EMPTY) { // eslint-disable-line no-unused-vars return arg; } + /** + * Lifecycle hook executed after clear operation for custom postprocessing + * @returns {void} Override this method in subclasses to implement custom logic + * @example + * class MyStore extends Haro { + * onclear() { + * console.log('Store cleared'); + * } + * } + */ onclear () { // Hook for custom logic after clear; override in subclass if needed } - ondelete (key = STRING_EMPTY, batch = false) { - return [key, batch]; - } - - onoverride (type = STRING_EMPTY) { - return type; - } - - onset (arg = {}, batch = false) { - return [arg, batch]; - } - + /** + * Lifecycle hook executed after delete operation for custom postprocessing + * @param {string} [key=STRING_EMPTY] - Key of deleted record + * @param {boolean} [batch=false] - Whether this was part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + ondelete (key = STRING_EMPTY, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic after delete; override in subclass if needed + } + + /** + * Lifecycle hook executed after override operation for custom postprocessing + * @param {string} [type=STRING_EMPTY] - Type of override operation that was performed + * @returns {void} Override this method in subclasses to implement custom logic + */ + onoverride (type = STRING_EMPTY) { // eslint-disable-line no-unused-vars + // Hook for custom logic after override; override in subclass if needed + } + + /** + * Lifecycle hook executed after set operation for custom postprocessing + * @param {Object} [arg={}] - Record that was set + * @param {boolean} [batch=false] - Whether this was part of a batch operation + * @returns {void} Override this method in subclasses to implement custom logic + */ + onset (arg = {}, batch = false) { // eslint-disable-line no-unused-vars + // Hook for custom logic after set; override in subclass if needed + } + + /** + * Replaces all store data or indexes with new data for bulk operations + * @param {Array} data - Data to replace with (format depends on type) + * @param {string} [type=STRING_RECORDS] - Type of data: 'records' or 'indexes' + * @returns {boolean} True if operation succeeded + * @throws {Error} Throws error if type is invalid + * @example + * const records = [['key1', {name: 'John'}], ['key2', {name: 'Jane'}]]; + * store.override(records, 'records'); + */ override (data, type = STRING_RECORDS) { const result = true; - if (type === STRING_INDEXES) { this.indexes = new Map(data.map(i => [i[0], new Map(i[1].map(ii => [ii[0], new Set(ii[1])]))])); } else if (type === STRING_RECORDS) { @@ -273,67 +607,109 @@ export class Haro { } else { throw new Error(STRING_INVALID_TYPE); } - this.onoverride(type); return result; } - reduce (fn, accumulator, raw = false) { - let a = accumulator ?? this.data.keys().next().value; - + /** + * Reduces all records to a single value using a reducer function + * @param {Function} fn - Reducer function (accumulator, value, key, store) + * @param {*} [accumulator] - Initial accumulator value + * @returns {*} Final reduced value + * @example + * const totalAge = store.reduce((sum, record) => sum + record.age, 0); + * const names = store.reduce((acc, record) => acc.concat(record.name), []); + */ + reduce (fn, accumulator = []) { + let a = accumulator; this.forEach((v, k) => { - a = fn(a, v, k, this, raw); + a = fn(a, v, k, this); }, this); return a; } + /** + * Rebuilds indexes for specified fields or all fields for data consistency + * @param {string|string[]} [index] - Specific index field(s) to rebuild, or all if not specified + * @returns {Haro} This instance for method chaining + * @example + * store.reindex(); // Rebuild all indexes + * store.reindex('name'); // Rebuild only name index + * store.reindex(['name', 'email']); // Rebuild name and email indexes + */ reindex (index) { const indices = index ? [index] : this.index; - if (index && this.index.includes(index) === false) { this.index.push(index); } - this.each(indices, i => this.indexes.set(i, new Map())); - this.forEach((data, key) => this.each(indices, i => this.setIndex(this.index, this.indexes, this.delimiter, key, data, i))); + this.forEach((data, key) => this.each(indices, i => this.setIndex(key, data, i))); return this; } + /** + * Searches for records containing a value across specified indexes + * @param {*} value - Value to search for (string, function, or RegExp) + * @param {string|string[]} [index] - Index(es) to search in, or all if not specified + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of matching records + * @example + * const results = store.search('john'); // Search all indexes + * const nameResults = store.search('john', 'name'); // Search only name index + * const regexResults = store.search(/^admin/, 'role'); // Regex search + */ search (value, index, raw = false) { - const result = new Map(), - fn = typeof value === STRING_FUNCTION, - rgex = value && typeof value.test === STRING_FUNCTION; - - if (value) { - this.each(index ? Array.isArray(index) ? index : [index] : this.index, i => { - let idx = this.indexes.get(i); - - if (idx) { - idx.forEach((lset, lkey) => { - switch (true) { - case fn && value(lkey, i): - case rgex && value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey): - case lkey === value: - lset.forEach(key => { - if (result.has(key) === false && this.data.has(key)) { - result.set(key, this.get(key, raw)); - } - }); - break; - default: - void 0; + const result = new Set(); // Use Set for unique keys + const fn = typeof value === STRING_FUNCTION; + const rgex = value && typeof value.test === STRING_FUNCTION; + if (!value) return this.immutable ? this.freeze() : []; + const indices = index ? Array.isArray(index) ? index : [index] : this.index; + for (const i of indices) { + const idx = this.indexes.get(i); + if (idx) { + for (const [lkey, lset] of idx) { + let match = false; + + if (fn) { + match = value(lkey, i); + } else if (rgex) { + match = value.test(Array.isArray(lkey) ? lkey.join(STRING_COMMA) : lkey); + } else { + match = lkey === value; + } + + if (match) { + for (const key of lset) { + if (this.data.has(key)) { + result.add(key); + } } - }); + } } - }); + } + } + let records = Array.from(result).map(key => this.get(key, raw)); + if (!raw && this.immutable) { + records = Object.freeze(records); } - return raw ? Array.from(result.values()) : this.list(...Array.from(result.values())); + return records; } + /** + * Sets or updates a record in the store with automatic indexing + * @param {string|null} [key=null] - Key for the record, or null to use record's key field + * @param {Object} [data={}] - Record data to set + * @param {boolean} [batch=false] - Whether this is part of a batch operation + * @param {boolean} [override=false] - Whether to override existing data instead of merging + * @returns {Object} The stored record (frozen if immutable mode) + * @example + * const user = store.set(null, {name: 'John', age: 30}); // Auto-generate key + * const updated = store.set('user123', {age: 31}); // Update existing record + */ set (key = null, data = {}, batch = false, override = false) { if (key === null) { key = data[this.key] ?? this.uuid(); @@ -346,7 +722,7 @@ export class Haro { } } else { const og = this.get(key, true); - this.delIndex(this.index, this.indexes, this.delimiter, key, og); + this.deleteIndex(key, og); if (this.versioning) { this.versions.get(key).add(Object.freeze(this.clone(og))); } @@ -355,66 +731,128 @@ export class Haro { } } this.data.set(key, x); - this.setIndex(this.index, this.indexes, this.delimiter, key, x, null); + this.setIndex(key, x, null); const result = this.get(key); this.onset(result, batch); return result; } - setIndex (index, indexes, delimiter, key, data, indice) { - this.each(indice === null ? index : [indice], i => { - let lindex = indexes.get(i); - if (!lindex) { - lindex = new Map(); - indexes.set(i, lindex); + /** + * Internal method to add entries to indexes for a record + * @param {string} key - Key of record being indexed + * @param {Object} data - Data of record being indexed + * @param {string|null} indice - Specific index to update, or null for all + * @returns {Haro} This instance for method chaining + */ + setIndex (key, data, indice) { + this.each(indice === null ? this.index : [indice], i => { + let idx = this.indexes.get(i); + if (!idx) { + idx = new Map(); + this.indexes.set(i, idx); } - if (i.includes(delimiter)) { - this.each(this.indexKeys(i, delimiter, data), c => { - if (!lindex.has(c)) { - lindex.set(c, new Set()); - } - lindex.get(c).add(key); - }); + const fn = c => { + if (!idx.has(c)) { + idx.set(c, new Set()); + } + idx.get(c).add(key); + }; + if (i.includes(this.delimiter)) { + this.each(this.indexKeys(i, this.delimiter, data), fn); } else { - this.each(Array.isArray(data[i]) ? data[i] : [data[i]], d => { - if (!lindex.has(d)) { - lindex.set(d, new Set()); - } - lindex.get(d).add(key); - }); + this.each(Array.isArray(data[i]) ? data[i] : [data[i]], fn); } }); + + return this; } - sort (fn, frozen = true) { - return frozen ? Object.freeze(this.limit(INT_0, this.data.size, true).sort(fn).map(i => Object.freeze(i))) : this.limit(INT_0, this.data.size, true).sort(fn); + /** + * Sorts all records using a comparator function + * @param {Function} fn - Comparator function for sorting (a, b) => number + * @param {boolean} [frozen=false] - Whether to return frozen records + * @returns {Array} Sorted array of records + * @example + * const sorted = store.sort((a, b) => a.age - b.age); // Sort by age + * const names = store.sort((a, b) => a.name.localeCompare(b.name)); // Sort by name + */ + sort (fn, frozen = false) { + const dataSize = this.data.size; + let result = this.limit(INT_0, dataSize, true).sort(fn); + if (frozen) { + result = this.freeze(...result); + } + + return result; } + /** + * Comparator function for sorting keys with type-aware comparison logic + * @param {*} a - First value to compare + * @param {*} b - Second value to compare + * @returns {number} Negative number if a < b, positive if a > b, zero if equal + * @example + * const keys = ['name', 'age', 'email']; + * keys.sort(store.sortKeys); // Alphabetical sort + * + * const mixed = [10, '5', 'abc', 3]; + * mixed.sort(store.sortKeys); // Type-aware sort: numbers first, then strings + */ + sortKeys (a, b) { + // Handle string comparison + if (typeof a === STRING_STRING && typeof b === STRING_STRING) { + return a.localeCompare(b); + } + // Handle numeric comparison + if (typeof a === STRING_NUMBER && typeof b === STRING_NUMBER) { + return a - b; + } + + // Handle mixed types or other types by converting to string + + return String(a).localeCompare(String(b)); + } + + /** + * Sorts records by a specific indexed field in ascending order + * @param {string} [index=STRING_EMPTY] - Index field name to sort by + * @param {boolean} [raw=false] - Whether to return raw data without processing + * @returns {Array} Array of records sorted by the specified field + * @throws {Error} Throws error if index field is empty or invalid + * @example + * const byAge = store.sortBy('age'); + * const byName = store.sortBy('name'); + */ sortBy (index = STRING_EMPTY, raw = false) { if (index === STRING_EMPTY) { throw new Error(STRING_INVALID_FIELD); } - - const result = [], - keys = []; - + let result = []; + const keys = []; if (this.indexes.has(index) === false) { this.reindex(index); } - const lindex = this.indexes.get(index); - lindex.forEach((idx, key) => keys.push(key)); - this.each(keys.sort(), i => lindex.get(i).forEach(key => result.push(this.get(key, raw)))); + this.each(keys.sort(this.sortKeys), i => lindex.get(i).forEach(key => result.push(this.get(key, raw)))); + if (this.immutable) { + result = Object.freeze(result); + } - return raw ? result : this.list(...result); + return result; } - toArray (frozen = true) { + /** + * Converts all store data to a plain array of records + * @returns {Array} Array containing all records in the store + * @example + * const allRecords = store.toArray(); + * console.log(`Store contains ${allRecords.length} records`); + */ + toArray () { const result = Array.from(this.data.values()); - - if (frozen) { + if (this.immutable) { this.each(result, i => Object.freeze(i)); Object.freeze(result); } @@ -422,61 +860,142 @@ export class Haro { return result; } + /** + * Generates a RFC4122 v4 UUID for record identification + * @returns {string} UUID string in standard format + * @example + * const id = store.uuid(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479" + */ uuid () { return uuid(); } + /** + * Returns an iterator of all values in the store + * @returns {Iterator} Iterator of record values + * @example + * for (const record of store.values()) { + * console.log(record.name); + * } + */ values () { return this.data.values(); } - where (predicate = {}, raw = false, op = STRING_DOUBLE_PIPE) { - const keys = this.index.filter(i => i in predicate); + /** + * Internal helper method for predicate matching with support for arrays and regex + * @param {Object} record - Record to test against predicate + * @param {Object} predicate - Predicate object with field-value pairs + * @param {string} op - Operator for array matching ('||' for OR, '&&' for AND) + * @returns {boolean} True if record matches predicate criteria + */ + matchesPredicate (record, predicate, op) { + const keys = Object.keys(predicate); + + return keys.every(key => { + const pred = predicate[key]; + const val = record[key]; + if (Array.isArray(pred)) { + if (Array.isArray(val)) { + return op === STRING_DOUBLE_AND ? pred.every(p => val.includes(p)) : pred.some(p => val.includes(p)); + } else { + return op === STRING_DOUBLE_AND ? pred.every(p => val === p) : pred.some(p => val === p); + } + } else if (pred instanceof RegExp) { + if (Array.isArray(val)) { + return op === STRING_DOUBLE_AND ? val.every(v => pred.test(v)) : val.some(v => pred.test(v)); + } else { + return pred.test(val); + } + } else if (Array.isArray(val)) { + return val.includes(pred); + } else { + return val === pred; + } + }); + } + /** + * Advanced filtering with predicate logic supporting AND/OR operations on arrays + * @param {Object} [predicate={}] - Object with field-value pairs for filtering + * @param {string} [op=STRING_DOUBLE_PIPE] - Operator for array matching ('||' for OR, '&&' for AND) + * @returns {Array} Array of records matching the predicate criteria + * @example + * // Find records with tags containing 'admin' OR 'user' + * const users = store.where({tags: ['admin', 'user']}, '||'); + * + * // Find records with ALL specified tags + * const powerUsers = store.where({tags: ['admin', 'power']}, '&&'); + * + * // Regex matching + * const emails = store.where({email: /^admin@/}); + */ + where (predicate = {}, op = STRING_DOUBLE_PIPE) { + const keys = this.index.filter(i => i in predicate); if (keys.length === 0) return []; - // Supported operators: '||' (OR), '&&' (AND) - // Always AND across fields (all keys must match for a record) - return this.filter(a => { - const matches = keys.map(i => { - const pred = predicate[i]; - const val = a[i]; + // Try to use indexes for better performance + const indexedKeys = keys.filter(k => this.indexes.has(k)); + if (indexedKeys.length > 0) { + // Use index-based filtering for better performance + let candidateKeys = new Set(); + let first = true; + for (const key of indexedKeys) { + const pred = predicate[key]; + const idx = this.indexes.get(key); + const matchingKeys = new Set(); if (Array.isArray(pred)) { - if (Array.isArray(val)) { - if (op === "&&") { - return pred.every(p => val.includes(p)); - } else { - return pred.some(p => val.includes(p)); + for (const p of pred) { + if (idx.has(p)) { + for (const k of idx.get(p)) { + matchingKeys.add(k); + } } - } else if (op === "&&") { - return pred.every(p => val === p); - } else { - return pred.some(p => val === p); } - } else if (pred instanceof RegExp) { - if (Array.isArray(val)) { - if (op === "&&") { - return val.every(v => pred.test(v)); - } else { - return val.some(v => pred.test(v)); - } - } else { - return pred.test(val); + } else if (idx.has(pred)) { + for (const k of idx.get(pred)) { + matchingKeys.add(k); } - } else if (Array.isArray(val)) { - return val.includes(pred); + } + if (first) { + candidateKeys = matchingKeys; + first = false; } else { - return val === pred; + // AND operation across different fields + candidateKeys = new Set([...candidateKeys].filter(k => matchingKeys.has(k))); } - }); - const isMatch = matches.every(Boolean); + } + // Filter candidates with full predicate logic + const results = []; + for (const key of candidateKeys) { + const record = this.get(key, true); + if (this.matchesPredicate(record, predicate, op)) { + results.push(this.immutable ? this.get(key) : record); + } + } - return isMatch; - }, raw); - } + return this.immutable ? this.freeze(...results) : results; + } + // Fallback to full scan if no indexes available + return this.filter(a => this.matchesPredicate(a, predicate, op)); + } } +/** + * Factory function to create a new Haro instance with optional initial data + * @param {Array|null} [data=null] - Initial data to populate the store + * @param {Object} [config={}] - Configuration object passed to Haro constructor + * @returns {Haro} New Haro instance configured and optionally populated + * @example + * const store = haro([ + * {id: 1, name: 'John', age: 30}, + * {id: 2, name: 'Jane', age: 25} + * ], { + * index: ['name', 'age'], + * versioning: true + * }); + */ export function haro (data = null, config = {}) { const obj = new Haro(config); diff --git a/src/uuid.js b/src/uuid.js deleted file mode 100644 index be91d350..00000000 --- a/src/uuid.js +++ /dev/null @@ -1,16 +0,0 @@ -import {INT_0, INT_1, INT_16, INT_3, INT_4, INT_8, INT_9, STRING_A, STRING_B, STRING_OBJECT} from "./constants.js"; - -/* istanbul ignore next */ -const r = [INT_8, INT_9, STRING_A, STRING_B]; - -/* istanbul ignore next */ -function s () { - return ((Math.random() + INT_1) * 0x10000 | INT_0).toString(INT_16).substring(INT_1); -} - -/* istanbul ignore next */ -function randomUUID () { - return `${s()}${s()}-${s()}-4${s().slice(INT_0, INT_3)}-${r[Math.floor(Math.random() * INT_4)]}${s().slice(INT_0, INT_3)}-${s()}${s()}${s()}`; -} - -export const uuid = typeof crypto === STRING_OBJECT ? crypto.randomUUID.bind(crypto) : randomUUID; diff --git a/test/data.json b/test/data.json deleted file mode 100644 index aa2dc36e..00000000 --- a/test/data.json +++ /dev/null @@ -1,254 +0,0 @@ -[ - { - "id": 0, - "guid": "8385ac94-0ebf-4a83-a6ba-25b54ce343be", - "isActive": false, - "balance": "$2,004.00", - "picture": "http://placehold.it/32x32", - "age": 20, - "name": "Decker Merrill", - "gender": "male", - "company": "Insectus", - "email": "deckermerrill@insectus.com", - "phone": "+1 (915) 493-2548", - "address": "289 Clarkson Avenue, Tecolotito, North Carolina, 2312", - "about": "Irure qui ipsum in et velit occaecat sit aliquip amet. Proident commodo laboris et cupidatat et cillum magna nulla elit dolor ullamco sunt. Ex voluptate esse elit ad amet proident ipsum cupidatat anim eu in enim quis Lorem. Et Lorem ullamco id veniam Lorem non amet et ullamco sunt consequat laboris.\r\n", - "registered": "1988-05-09T11:19:57 +04:00", - "latitude": -16.336528, - "longitude": -164.594492, - "tags": [ - "sunt", - "velit", - "occaecat", - "in", - "esse", - "qui", - "quis" - ], - "friends": [ - { - "id": 0, - "name": "Vaughan Banks" - }, - { - "id": 1, - "name": "Francine Moore" - }, - { - "id": 2, - "name": "Randolph Mcbride" - } - ], - "randomArrayItem": "lemon" - }, - { - "id": 1, - "guid": "f19b2c40-f503-4ccf-b1f7-454bf7fca45b", - "isActive": true, - "balance": "$2,754.00", - "picture": "http://placehold.it/32x32", - "age": 24, - "name": "Waters Yates", - "gender": "male", - "company": "Coash", - "email": "watersyates@coash.com", - "phone": "+1 (986) 458-3274", - "address": "927 Glenmore Avenue, Hoehne, Louisiana, 8956", - "about": "Veniam non ad ipsum nulla. Velit reprehenderit in proident cupidatat anim est. Eiusmod est adipisicing officia nulla ex minim ipsum aliqua ullamco incididunt ut commodo irure. Deserunt dolor culpa qui ex.\r\n", - "registered": "1995-10-02T21:01:33 +04:00", - "latitude": 33.650214, - "longitude": 88.119372, - "tags": [ - "est", - "culpa", - "ullamco", - "tempor", - "irure", - "cillum", - "eiusmod" - ], - "friends": [ - { - "id": 0, - "name": "Kline Adams" - }, - { - "id": 1, - "name": "Serrano Beck" - }, - { - "id": 2, - "name": "Leach Pittman" - } - ], - "randomArrayItem": "cherry" - }, - { - "id": 2, - "guid": "f34d994b-24eb-4553-adf7-8f61e7ef8741", - "isActive": false, - "balance": "$1,774.00", - "picture": "http://placehold.it/32x32", - "age": 26, - "name": "Elnora Durham", - "gender": "female", - "company": "Spherix", - "email": "elnoradurham@spherix.com", - "phone": "+1 (943) 566-2184", - "address": "604 Interborough Parkway, Marne, Kansas, 8905", - "about": "Tempor quis voluptate nulla id consequat. Culpa laboris esse aliquip veniam esse duis ea. Magna nostrud occaecat commodo id officia mollit deserunt nostrud excepteur. Nisi reprehenderit mollit ipsum tempor. Excepteur qui sint Lorem cupidatat incididunt velit. Nostrud amet pariatur anim occaecat excepteur in mollit. Voluptate aliquip et ut minim tempor labore labore mollit sit incididunt laboris aliquip id mollit.\r\n", - "registered": "2002-11-21T10:40:51 +05:00", - "latitude": 5.972226, - "longitude": -114.190562, - "tags": [ - "sunt", - "veniam", - "occaecat", - "ad", - "elit", - "adipisicing", - "nisi" - ], - "friends": [ - { - "id": 0, - "name": "Meyers Franklin" - }, - { - "id": 1, - "name": "Quinn Willis" - }, - { - "id": 2, - "name": "Guthrie Burton" - } - ], - "randomArrayItem": "lemon" - }, - { - "id": 3, - "guid": "a94c8560-7bfd-42ec-a759-cbd5899b33c0", - "isActive": false, - "balance": "$3,013.00", - "picture": "http://placehold.it/32x32", - "age": 29, - "name": "Krista Adkins", - "gender": "female", - "company": "Genesynk", - "email": "kristaadkins@genesynk.com", - "phone": "+1 (952) 558-2099", - "address": "773 Kingsland Avenue, Dragoon, Iowa, 8282", - "about": "Incididunt eu duis eiusmod excepteur proident. Adipisicing ullamco Lorem aliqua officia velit consectetur mollit aliqua nulla. Lorem eu nulla do incididunt id occaecat commodo sit.\r\n", - "registered": "1994-10-25T21:35:12 +04:00", - "latitude": 17.447311, - "longitude": -37.719344, - "tags": [ - "aliqua", - "ea", - "dolore", - "veniam", - "sit", - "cillum", - "irure" - ], - "friends": [ - { - "id": 0, - "name": "Shaw Giles" - }, - { - "id": 1, - "name": "Ryan Sexton" - }, - { - "id": 2, - "name": "Patterson Dodson" - } - ], - "randomArrayItem": "apple" - }, - { - "id": 4, - "guid": "2a30000f-92dc-405c-b1e0-7c416d766b39", - "isActive": false, - "balance": "$2,224.00", - "picture": "http://placehold.it/32x32", - "age": 24, - "name": "Mcneil Weiss", - "gender": "male", - "company": "Magmina", - "email": "mcneilweiss@magmina.com", - "phone": "+1 (896) 490-2500", - "address": "983 Malta Street, Wheatfields, Utah, 3940", - "about": "Eiusmod est duis duis esse cillum Lorem anim nulla ex. Quis mollit commodo aliqua eu voluptate ut incididunt nostrud irure velit exercitation magna duis. Incididunt duis aliqua mollit magna dolor ipsum velit quis nisi. Exercitation exercitation eiusmod commodo occaecat labore id aute consectetur magna. Incididunt eiusmod excepteur ut laborum eu nostrud exercitation minim eu sint est.\r\n", - "registered": "2005-07-28T05:17:15 +04:00", - "latitude": 32.950276, - "longitude": -85.790254, - "tags": [ - "deserunt", - "et", - "adipisicing", - "officia", - "officia", - "irure", - "officia" - ], - "friends": [ - { - "id": 0, - "name": "Collier Floyd" - }, - { - "id": 1, - "name": "Jennifer Newton" - }, - { - "id": 2, - "name": "Beryl Torres" - } - ], - "randomArrayItem": "apple" - }, - { - "id": 5, - "guid": "9e81813b-d223-4176-aa21-c538fac7a30f", - "isActive": true, - "balance": "$1,711.00", - "picture": "http://placehold.it/32x32", - "age": 20, - "name": "Leann Sosa", - "gender": "female", - "company": "Ecratic", - "email": "leannsosa@ecratic.com", - "phone": "+1 (812) 533-2766", - "address": "579 Joval Court, Esmont, Maine, 8184", - "about": "Officia nisi velit quis enim laborum sint aliquip cupidatat consequat velit dolor ex. Velit tempor ut ea exercitation sint veniam ex anim sint duis est excepteur nulla. Sint fugiat deserunt veniam excepteur officia occaecat adipisicing. Nisi velit enim sint cupidatat nulla pariatur quis do ipsum labore cupidatat fugiat anim labore.\r\n", - "registered": "1995-06-20T00:25:56 +04:00", - "latitude": 22.078592, - "longitude": -0.305317, - "tags": [ - "mollit", - "enim", - "dolore", - "sunt", - "ex", - "excepteur", - "aliqua" - ], - "friends": [ - { - "id": 0, - "name": "Cox Meyer" - }, - { - "id": 1, - "name": "Stokes Blackwell" - }, - { - "id": 2, - "name": "Bridges Franco" - } - ], - "randomArrayItem": "cherry" - } -] \ No newline at end of file diff --git a/test/offline.js b/test/offline.js deleted file mode 100644 index e1df5c6d..00000000 --- a/test/offline.js +++ /dev/null @@ -1,463 +0,0 @@ -import assert from "node:assert"; -import {haro} from "../dist/haro.cjs"; -import {readFile} from "node:fs/promises"; - -const fileUrl = new URL("./data.json", import.meta.url); -const data = JSON.parse(await readFile(fileUrl, "utf8")); -const odata = data.map(i => [i.guid, i]); - -describe("Starting state", function () { - const store = haro(null, {key: "guid"}); - - it("should be empty", function () { - assert.strictEqual(store.size, 0); - assert.strictEqual(store.data.size, 0); - }); -}); - -describe("Create", function () { - const store = haro(null, {key: "guid"}); - - it("should have a matching size (single)", function () { - store.set(null, data[0]); - assert.strictEqual(store.size, 1); - assert.strictEqual(store.data.size, 1); - store.clear(); - }); - - it("should have a matching size (batch)", function () { - store.batch(data, "set"); - assert.strictEqual(store.size, 6); - assert.strictEqual(store.data.size, 6); - assert.strictEqual(store.registry.length, 6); - assert.strictEqual(store.limit(0, 2)[1][0], store.get(store.registry[1])[0]); - assert.strictEqual(store.limit(2, 4)[1][0], store.get(store.registry[3])[0]); - assert.strictEqual(store.limit(5, 10).length, 1); - assert.strictEqual(store.filter(function (i) { - return (/decker/i).test(i.name); - })[0].length, 2); - assert.strictEqual(store.filter(function (i) { - return (/decker/i).test(i.name); - }, true).length, 1); - assert.strictEqual(store.map(function (i) { - i.name = "John Doe"; - - return i; - }).length, 6); - assert.strictEqual(store.map(function (i) { - i.name = "John Doe"; - - return i; - })[0].name, "John Doe"); - }); -}); - -describe("Read", function () { - const store = haro(null, { - key: "guid", - index: ["name", "age", "age|gender", "company", "name", "tags", "company|tags"], - logging: false - }); - - it("should return an array (tuple) by default", function () { - const arg = store.set(null, data[0]), - record = store.get(arg[0]); - - assert.strictEqual(store.size, 1); - assert.strictEqual(store.data.size, 1); - assert.strictEqual(Object.keys(record[1]).length, 19); - assert.strictEqual(record[1].name, "Decker Merrill"); - store.clear(); - }); - - it("should return a record when specified", function () { - const arg = store.set(null, data[0]), - record = store.get(arg[0], true); - - assert.strictEqual(store.size, 1); - assert.strictEqual(store.data.size, 1); - assert.strictEqual(Object.keys(record).length, 19); - assert.strictEqual(record.name, "Decker Merrill"); - store.clear(); - }); - - it("should return 'null' for invalid 'key'", function () { - assert.strictEqual(store.get("abc") instanceof Array, true); - assert.strictEqual(store.get("abc")[0], "abc"); - assert.strictEqual(store.get("abc")[1], null); - assert.strictEqual(store.get("abc", true), null); - }); - - it("should return immutable records", function () { - const arg = store.set(null, data[0]); - - store.get(arg[0], true).guid += "a"; - assert.strictEqual(store.get(arg[0])[1].guid, arg[0]); - store.clear(); - }); - - it("should be able to return records via index", function () { - store.batch(data, "set"); - assert.strictEqual(store.find({name: "Decker Merrill"}).length, 1); - assert.strictEqual(store.find({age: 20}).length, 2); - assert.strictEqual(store.indexes.get("age").get(20).size, 2); - assert.strictEqual(store.find({age: 20, gender: "male"}).length, 1); - assert.strictEqual(store.find({gender: "male", age: 20}).length, 1); - assert.strictEqual(store.find({age: 50}).length, 0); - assert.strictEqual(store.find({agez: 1}).length, 0); - assert.strictEqual(store.limit(0, 3)[2][1].guid, "f34d994b-24eb-4553-adf7-8f61e7ef8741"); - store.del(store.find({age: 20, gender: "male"})[0][0]); - assert.strictEqual(store.find({age: 20, gender: "male"}).length, 0); - assert.strictEqual(store.indexes.get("age").get(20).size, 1); - assert.strictEqual(store.limit(0, 3)[2][1].guid, "a94c8560-7bfd-42ec-a759-cbd5899b33c0"); - store.clear(); - }); - - it("should support 'toArray()'", function () { - store.batch(data, "set"); - assert.strictEqual(store.toArray().length, 6); - assert.strictEqual(Object.isFrozen(store.toArray()), true); - assert.strictEqual(Object.isFrozen(store.toArray(false)), false); - store.clear(); - }); - - it("should be sortable via index", function () { - store.batch(data, "set"); - - // Sorting age descending - const arg = store.sort(function (a, b) { - return a.age > b.age ? -1 : a.age === b.age ? 0 : 1; - }); - - assert.strictEqual(arg[0].guid, "a94c8560-7bfd-42ec-a759-cbd5899b33c0"); - }); - - it("should be able to create indexes while sorting", function () { - store.batch(data, "set"); - assert.strictEqual(store.sortBy("company")[0][1].company, "Coash"); - store.clear(); - }); - - it("should return an empty array when searching when not indexed", function () { - store.batch(data, "set"); - assert.strictEqual(store.search(new RegExp(".*de.*", "i"), "x").length, 0); - store.clear(); - }); - - it("should return an array when searching an index", function () { - store.batch(data, "set"); - - const result1 = store.search(new RegExp(".*de.*", "i")), - result2 = store.search(20, "age"), - result3 = store.search(/velit/, "tags"); - - assert.strictEqual(result1.length, 2); - assert.strictEqual(result1[0][1].name, "Decker Merrill"); - assert.strictEqual(result2.length, 2); - assert.strictEqual(result2[0][1].name, "Decker Merrill"); - assert.strictEqual(result3.length, 1); - assert.strictEqual(result3[0][1].name, "Decker Merrill"); - store.clear(); - }); - - it("should return an array when dumping indexes", function () { - store.batch(data, "set"); - - const ldata = store.dump("indexes"); - - assert.strictEqual(Object.keys(ldata).length, 6); - assert.strictEqual(Object.isFrozen(ldata), false); - store.clear(); - }); - - it("should return an array when dumping records", function () { - store.batch(data, "set"); - - const ldata = store.dump(); - - assert.strictEqual(Object.keys(ldata).length, data.length); - assert.strictEqual(Object.isFrozen(ldata), false); - store.clear(); - }); - - it("should return array of records where attributes match predicate", function () { - store.batch(data, "set"); - assert.strictEqual(store.where({company: "Insectus", tags: "occaecat"}).length, 1); - assert.strictEqual(store.where({company: "Insectus", tags: ["sunt", "aaaa"]}, false, "&&").length, 0); - assert.strictEqual(store.where({company: /insectus/i, tags: "occaecat"}).length, 1); - assert.strictEqual(store.where({tags: ["sunt", "veniam"]}, false, "&&").length, 1); - assert.strictEqual(store.where({company: "Insectus", tags: "aaaaa"}).length, 0, "Should be '0'"); - assert.strictEqual(store.where({}).length, 0, "Should be '0'"); - }); - - it("should cover all predicate array and RegExp branches in Haro.where", function () { - store.batch([ - { guid: "a", tags: ["x", "y"], company: "Foo" }, - { guid: "b", tags: ["x", "z"], company: "Foo" }, - { guid: "c", tags: ["y", "z"], company: "Bar" }, - { guid: "d", tags: "z", company: "Bar" }, - { guid: "e", tags: "y", company: "Baz" }, - { guid: "f", tags: ["x", "y", "z"], company: "Baz" } - ], "set"); - - // pred is array, val is array, op === '&&' - assert.deepStrictEqual( - store.where({tags: ["x", "y"]}, true, "&&").map(r => r.guid).sort(), - ["a", "f"], - "Array pred/val with && should match only those containing all" - ); - - // pred is array, val is array, op !== '&&' - assert.ok(store.where({tags: ["x", "y"]}, true, "||").some(r => r.guid === "a"), "Array pred/val with || should match at least one"); - - // pred is array, val is not array, op === '&&' - assert.deepStrictEqual( - store.where({tags: ["y", "z"]}, true, "&&").map(r => r.guid).sort(), - ["c", "f"], - "Array pred, non-array val, &&" - ); - - // pred is array, val is not array, op !== '&&' - assert.ok(store.where({tags: ["y", "z"]}, true, "||").some(r => r.guid === "e"), "Array pred, non-array val, ||"); - - // pred is RegExp, val is array, op === '&&' - assert.deepStrictEqual( - store.where({tags: /x|y/, company: "Baz"}, true, "&&").map(r => r.guid).sort(), - ["e"], - "RegExp pred, array val, &&" - ); - - // pred is RegExp, val is array, op !== '&&' - assert.ok(store.where({tags: /x|y/, company: "Baz"}, true, "||").some(r => r.guid === "f"), "RegExp pred, array val, ||"); - }); -}); - -describe("Update", function () { - const store = haro(null, {key: "guid", versioning: true}); - - it("should have a matching size (single)", function () { - let arg = store.set(null, data[0]); - - assert.strictEqual(arg[1].name, "Decker Merrill"); - arg = store.set(arg[0], {name: "John Doe"}); - assert.strictEqual(arg[1].name, "John Doe"); - assert.strictEqual(store.versions.get(arg[0]).size, 1); - store.clear(); - }); - - it("should have a matching size (batch)", function () { - store.batch(data, "set"); - assert.strictEqual(store.size, 6); - assert.strictEqual(store.data.size, 6); - assert.strictEqual(store.registry.length, 6); - store.batch(data, "set"); - assert.strictEqual(store.size, 6); - assert.strictEqual(store.data.size, 6); - assert.strictEqual(store.registry.length, 6); - assert.strictEqual(store.limit(0, 2)[1][0], store.get(store.registry[1])[0]); - assert.strictEqual(store.limit(2, 4)[1][0], store.get(store.registry[3])[0]); - assert.strictEqual(store.limit(5, 10).length, 1); - assert.strictEqual(store.filter(function (i) { - return (/decker/i).test(i.name); - }).length, 1); - assert.strictEqual(store.map(function (i) { - i.name = "John Doe"; - - return i; - }).length, 6); - assert.strictEqual(store.map(function (i) { - i.name = "John Doe"; - - return i; - })[0].name, "John Doe"); - store.clear(); - }); - - it("should be support overriding indexes", function () { - store.batch(data, "set"); - store.override([ - ["name", [ - ["Decker Merrill", ["cfbfe5d1-451d-47b1-96c4-8e8e83fe9cfd"]], - ["Waters Yates", ["cbaa7d2f-b098-4347-9437-e1f879c9232a"]], - ["Elnora Durham", ["1adf114d-f0ab-4a29-9d28-47cd4a627127"]], - ["Krista Adkins", ["c5849290-afa2-4a33-a23f-64253f0d9ad9"]], - ["Mcneil Weiss", ["eccdbfd9-223f-4a85-a791-4567fecbeb44"]], - ["Leann Sosa", ["47ce98a7-3c4c-4175-9a9a-f32af8392065"]] - ]], - ["age", [ - [20, ["cfbfe5d1-451d-47b1-96c4-8e8e83fe9cfd", - "47ce98a7-3c4c-4175-9a9a-f32af8392065"]], - [24, ["cbaa7d2f-b098-4347-9437-e1f879c9232a", - "eccdbfd9-223f-4a85-a791-4567fecbeb44"]], - [26, ["1adf114d-f0ab-4a29-9d28-47cd4a627127"]], - [29, ["c5849290-afa2-4a33-a23f-64253f0d9ad9"]] - ]], - ["age|gender", [ - ["20|male", ["cfbfe5d1-451d-47b1-96c4-8e8e83fe9cfd"]], - ["24|male", ["cbaa7d2f-b098-4347-9437-e1f879c9232a", - "eccdbfd9-223f-4a85-a791-4567fecbeb44"]], - ["26|female", ["1adf114d-f0ab-4a29-9d28-47cd4a627127"]], - ["29|female", ["c5849290-afa2-4a33-a23f-64253f0d9ad9"]], - ["20|female", ["47ce98a7-3c4c-4175-9a9a-f32af8392065"]] - ]] - ], "indexes"); - - assert.strictEqual(store.indexes.size, 3); - assert.strictEqual(store.indexes.get("name").size, 6); - assert.strictEqual(store.indexes.get("age").size, 4); - assert.strictEqual(store.indexes.get("age").get(20).size, 2); - assert.strictEqual(store.indexes.get("age|gender").size, 5); - }); - - it("should be support overriding records", function () { - store.override(odata, "records"); - assert.strictEqual(store.size, 6); - assert.strictEqual(store.registry.length, 6); - assert.strictEqual(store.data.size, 6); - assert.strictEqual(store.data.get(store.registry[0], true).guid, data[0].guid); - }); -}); - -describe("Delete", function () { - const store = haro(null, {key: "guid", versioning: true}); - - it("should throw an error deleting an invalid key", function () { - assert.throws(() => store.del("invalid"), Error); - }); - - it("should have a matching size (single)", function () { - const arg = store.set(null, data[0]); - - assert.strictEqual(arg[1].name, "Decker Merrill"); - store.del(arg[0]); - assert.strictEqual(store.size, 0); - assert.strictEqual(store.data.size, 0); - store.clear(); - }); - - it("should have a matching size (batch)", function () { - const arg = store.batch(data, "set"); - - assert.strictEqual(arg[0][1].name, "Decker Merrill"); - store.batch([arg[0][0], arg[2][0]], "del"); - assert.strictEqual(store.size, 4); - assert.strictEqual(store.data.size, 4); - }); -}); - -describe("Filter", function () { - const store = haro(null, {key: "guid"}); - - it("should throw an error when not providing the function", function () { - assert.throws(() => store.filter(undefined, true), Error); - }); - - it("should filter to a record (single)", function () { - store.set(null, data[0]); - assert.strictEqual(store.filter(arg => arg.name === "Decker Merrill", true)[0].name, "Decker Merrill"); - assert.strictEqual(store.filter(arg => arg.name === "Decker Merrill", false)[0][1].name, "Decker Merrill"); - }); -}); - -describe("Has", function () { - const store = haro(null, {key: "guid"}); - - it("return a boolean", function () { - store.set(null, data[0]); - assert.strictEqual(store.has("abc"), false); - assert.strictEqual(store.has(Array.from(store.keys())[0]), true); - }); -}); - -describe("Map", function () { - const store = haro(null, {key: "guid"}); - - it("should throw an error when not providing the function", function () { - assert.throws(() => store.map(undefined, true), Error); - }); - - it("should map the records", function () { - store.set(null, data[0]); - assert.strictEqual(store.map(arg => arg.name, true)[0], "Decker Merrill"); - }); -}); - -describe("Merge", function () { - const store = haro(null, {key: "guid"}); - - it("should merge the inputs", function () { - assert.strictEqual(JSON.stringify(store.merge({a: {b: true}}, {a: {c: true}})), JSON.stringify({ - a: { - b: true, - c: true - } - })); - assert.strictEqual(JSON.stringify(store.merge({a: [1]}, {a: [2]})), JSON.stringify({a: [1, 2]})); - assert.strictEqual(JSON.stringify(store.merge({a: [1]}, {a: [2]}, true)), JSON.stringify({a: [2]})); - assert.strictEqual(JSON.stringify(store.merge({a: 1}, {a: 2})), JSON.stringify({a: 2})); - assert.strictEqual(JSON.stringify(store.merge({a: 1}, {a: null})), JSON.stringify({a: null})); - assert.strictEqual(JSON.stringify(store.merge({a: 1}, {a: undefined})), JSON.stringify({a: undefined})); - assert.strictEqual(JSON.stringify(store.merge([1], [2], true)), JSON.stringify([2])); - assert.strictEqual(JSON.stringify(store.merge([1], [2])), JSON.stringify([1, 2])); - assert.strictEqual(JSON.stringify(store.merge("a", "b")), JSON.stringify("b")); - assert.strictEqual(JSON.stringify(store.merge(1, 2)), JSON.stringify(2)); - assert.strictEqual(JSON.stringify(store.merge(true, false)), JSON.stringify(false)); - }); -}); - -describe("Override", function () { - const store = haro(null, {key: "guid"}); - - it("should throw an error when receiving invalid type", function () { - assert.throws(() => store.override(null, "invalid"), Error); - }); -}); - -describe("Reindex", function () { - const store = haro(null, {key: "guid"}); - - it("should add a missing index when re-indexing", function () { - store.set(null, data[0]); - store.reindex("latitude"); - }); -}); - -describe("Reindex", function () { - const store = haro(null, { key: "id", index: ["tags"] }); - - it("setIndex branch for missing index key", function () { - store.set("1", { id: "1", tags: ["a"] }); - store.indexes.delete("tags"); - store.setIndex(["tags"], store.indexes, "|", "1", { id: "1", tags: ["a"] }, "tags"); - assert.ok(store.indexes.get("tags")); - }); -}); - -describe("Sort By", function () { - const store = haro(null, {key: "guid"}); - - it("should throw an error when receiving invalid field", function () { - assert.throws(() => store.sortBy(undefined, true), Error); - }); - - it("should add a missing index when re-indexing", function () { - store.sortBy("latitude", true); - }); -}); - -describe("Values", function () { - const store = haro(null, {key: "guid"}); - - it("should return an iterator of the values", function () { - store.set(null, data[0]); - assert.strictEqual(Array.from(store.values())[0].name, "Decker Merrill"); - }); -}); - -describe("Initial data", function () { - const store = haro([data[0]], {key: "guid"}); - - it("contain records when receiving an array", function () { - assert.strictEqual(store.size, 1); - }); -}); diff --git a/tests/unit/batch.test.js b/tests/unit/batch.test.js new file mode 100644 index 00000000..21d1f6b9 --- /dev/null +++ b/tests/unit/batch.test.js @@ -0,0 +1,68 @@ +import assert from "node:assert"; +import {describe, it} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Batch Operations", () => { + describe("batch()", () => { + it("should batch set multiple records", () => { + // Create a store with beforeBatch that returns the arguments + const batchStore = new class extends Haro { + beforeBatch (args) { + return args; + } + onbatch (result) { + return result; + } + }(); + + const data = [ + {id: "user1", name: "John", age: 30}, + {id: "user2", name: "Jane", age: 25} + ]; + const results = batchStore.batch(data, "set"); + + assert.strictEqual(results.length, 2); + assert.strictEqual(batchStore.size, 2); + assert.strictEqual(batchStore.has("user1"), true); + assert.strictEqual(batchStore.has("user2"), true); + }); + + it("should batch delete multiple records", () => { + // Create a store with beforeBatch that returns the arguments + const batchStore = new class extends Haro { + beforeBatch (args) { + return args; + } + onbatch (result) { + return result; + } + }(); + + batchStore.set("user1", {id: "user1", name: "John"}); + batchStore.set("user2", {id: "user2", name: "Jane"}); + + const results = batchStore.batch(["user1", "user2"], "del"); + + assert.strictEqual(results.length, 2); + assert.strictEqual(batchStore.size, 0); + }); + + it("should default to set operation", () => { + // Create a store with beforeBatch that returns the arguments + const batchStore = new class extends Haro { + beforeBatch (args) { + return args; + } + onbatch (result) { + return result; + } + }(); + + const data = [{id: "user1", name: "John"}]; + const results = batchStore.batch(data); + + assert.strictEqual(results.length, 1); + assert.strictEqual(batchStore.size, 1); + }); + }); +}); diff --git a/tests/unit/constructor.test.js b/tests/unit/constructor.test.js new file mode 100644 index 00000000..8d7f64da --- /dev/null +++ b/tests/unit/constructor.test.js @@ -0,0 +1,50 @@ +import assert from "node:assert"; +import {describe, it} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Constructor", () => { + it("should create a new instance with default configuration", () => { + const instance = new Haro(); + assert.strictEqual(instance.delimiter, "|"); + assert.strictEqual(instance.immutable, false); + assert.deepStrictEqual(instance.index, []); + assert.strictEqual(instance.key, "id"); + assert.strictEqual(instance.versioning, false); + assert.strictEqual(instance.size, 0); + assert.deepStrictEqual(instance.registry, []); + }); + + it("should create instance with custom configuration", () => { + const config = { + delimiter: "::", + immutable: true, + index: ["name", "email"], + key: "userId", + versioning: true + }; + const instance = new Haro(config); + + assert.strictEqual(instance.delimiter, "::"); + assert.strictEqual(instance.immutable, true); + assert.deepStrictEqual(instance.index, ["name", "email"]); + assert.strictEqual(instance.key, "userId"); + assert.strictEqual(instance.versioning, true); + }); + + it("should generate unique id when not provided", () => { + const instance1 = new Haro(); + const instance2 = new Haro(); + assert.notStrictEqual(instance1.id, instance2.id); + }); + + it("should use provided id", () => { + const customId = "custom-store-id"; + const instance = new Haro({id: customId}); + assert.strictEqual(instance.id, customId); + }); + + it("should handle non-array index configuration", () => { + const instance = new Haro({index: "name"}); + assert.deepStrictEqual(instance.index, []); + }); +}); diff --git a/tests/unit/crud.test.js b/tests/unit/crud.test.js new file mode 100644 index 00000000..38aaab91 --- /dev/null +++ b/tests/unit/crud.test.js @@ -0,0 +1,162 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Basic CRUD Operations", () => { + let store; + + beforeEach(() => { + store = new Haro(); + }); + + describe("set()", () => { + it("should set a record with auto-generated key", () => { + const data = {name: "John", age: 30}; + const result = store.set(null, data); + + assert.strictEqual(typeof result[0], "string"); + assert.strictEqual(result[1].name, "John"); + assert.strictEqual(result[1].age, 30); + assert.strictEqual(store.size, 1); + }); + + it("should set a record with specific key", () => { + const data = {id: "user123", name: "John", age: 30}; + const result = store.set("user123", data); + + assert.strictEqual(result[0], "user123"); + assert.strictEqual(result[1].name, "John"); + assert.strictEqual(result[1].age, 30); + }); + + it("should use record key field when key is null", () => { + const data = {id: "user456", name: "Jane", age: 25}; + const result = store.set(null, data); + + assert.strictEqual(result[0], "user456"); + assert.strictEqual(result[1].name, "Jane"); + }); + + it("should merge with existing record by default", () => { + store.set("user1", {id: "user1", name: "John", age: 30}); + const result = store.set("user1", {age: 31, city: "NYC"}); + + assert.strictEqual(result[1].name, "John"); + assert.strictEqual(result[1].age, 31); + assert.strictEqual(result[1].city, "NYC"); + }); + + it("should override existing record when override is true", () => { + store.set("user1", {id: "user1", name: "John", age: 30}); + const result = store.set("user1", {id: "user1", age: 31}, false, true); + + assert.strictEqual(result[1].name, undefined); + assert.strictEqual(result[1].age, 31); + }); + }); + + describe("get()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", name: "John", age: 30}); + }); + + it("should retrieve existing record", () => { + const result = store.get("user1"); + assert.strictEqual(result[0], "user1"); + assert.strictEqual(result[1].name, "John"); + }); + + it("should return null for non-existent record", () => { + const result = store.get("nonexistent"); + assert.strictEqual(result, null); + }); + + it("should return raw data when raw=true", () => { + const result = store.get("user1", true); + assert.strictEqual(result.name, "John"); + assert.strictEqual(result.age, 30); + }); + + it("should return frozen data in immutable mode", () => { + const immutableStore = new Haro({immutable: true}); + immutableStore.set("user1", {id: "user1", name: "John"}); + const result = immutableStore.get("user1"); + + assert.strictEqual(Object.isFrozen(result), true); + assert.strictEqual(Object.isFrozen(result[1]), true); + }); + }); + + describe("has()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", name: "John"}); + }); + + it("should return true for existing record", () => { + assert.strictEqual(store.has("user1"), true); + }); + + it("should return false for non-existent record", () => { + assert.strictEqual(store.has("nonexistent"), false); + }); + }); + + describe("delete()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", name: "John"}); + store.set("user2", {id: "user2", name: "Jane"}); + }); + + it("should delete existing record", () => { + store.delete("user1"); + assert.strictEqual(store.has("user1"), false); + assert.strictEqual(store.size, 1); + }); + + it("should throw error when deleting non-existent record", () => { + assert.throws(() => { + store.delete("nonexistent"); + }, /Record not found/); + }); + + it("should remove record from indexes", () => { + const indexedStore = new Haro({index: ["name"]}); + indexedStore.set("user1", {id: "user1", name: "John"}); + indexedStore.delete("user1"); + + const results = indexedStore.find({name: "John"}); + assert.strictEqual(results.length, 0); + }); + }); + + describe("clear()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", name: "John"}); + store.set("user2", {id: "user2", name: "Jane"}); + }); + + it("should remove all records", () => { + store.clear(); + assert.strictEqual(store.size, 0); + assert.deepStrictEqual(store.registry, []); + }); + + it("should clear all indexes", () => { + const indexedStore = new Haro({index: ["name"]}); + indexedStore.set("user1", {id: "user1", name: "John"}); + indexedStore.clear(); + + const results = indexedStore.find({name: "John"}); + assert.strictEqual(results.length, 0); + }); + + it("should clear versions when versioning is enabled", () => { + const versionedStore = new Haro({versioning: true}); + versionedStore.set("user1", {id: "user1", name: "John"}); + versionedStore.set("user1", {id: "user1", name: "John Updated"}); + versionedStore.clear(); + + assert.strictEqual(versionedStore.versions.size, 0); + }); + }); +}); diff --git a/tests/unit/error-handling.test.js b/tests/unit/error-handling.test.js new file mode 100644 index 00000000..c202d70d --- /dev/null +++ b/tests/unit/error-handling.test.js @@ -0,0 +1,41 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Error Handling", () => { + let store; + + beforeEach(() => { + store = new Haro(); + }); + + it("should handle invalid function in filter", () => { + assert.throws(() => { + store.filter(123); + }, /Invalid function/); + }); + + it("should handle invalid function in map", () => { + assert.throws(() => { + store.map("not a function"); + }, /Invalid function/); + }); + + it("should handle invalid field in sortBy", () => { + assert.throws(() => { + store.sortBy(""); + }, /Invalid field/); + }); + + it("should handle invalid type in override", () => { + assert.throws(() => { + store.override([], "invalid"); + }, /Invalid type/); + }); + + it("should handle record not found in delete", () => { + assert.throws(() => { + store.delete("nonexistent"); + }, /Record not found/); + }); +}); diff --git a/tests/unit/factory.test.js b/tests/unit/factory.test.js new file mode 100644 index 00000000..38bd591c --- /dev/null +++ b/tests/unit/factory.test.js @@ -0,0 +1,107 @@ +import assert from "node:assert"; +import {describe, it} from "mocha"; +import {Haro, haro} from "../../src/haro.js"; + +describe("haro factory function", () => { + it("should create new Haro instance", () => { + const store = haro(); + assert.strictEqual(store instanceof Haro, true); + }); + + it("should create instance with configuration", () => { + const config = {key: "userId", index: ["name"]}; + const store = haro(null, config); + assert.strictEqual(store.key, "userId"); + assert.deepStrictEqual(store.index, ["name"]); + }); + + it("should populate with initial data", () => { + const data = [ + {id: "user1", name: "John"}, + {id: "user2", name: "Jane"} + ]; + + // Create a config with a custom beforeBatch that returns the arguments + const config = { + beforeBatch: function (args) { + return args; + } + }; + + // Create the store and manually override the beforeBatch method + const store = haro(null, config); + store.beforeBatch = function (args) { + return args; + }; + + // Now batch the data + store.batch(data); + + assert.strictEqual(store.size, 2); + assert.strictEqual(store.has("user1"), true); + assert.strictEqual(store.has("user2"), true); + }); + + it("should handle null data", () => { + const store = haro(null); + assert.strictEqual(store.size, 0); + }); + + it("should combine initial data with configuration", () => { + const data = [{id: "user1", name: "John", age: 30}]; + const config = {index: ["name", "age"]}; + + // Create the store and manually override the beforeBatch method + const store = haro(null, config); + store.beforeBatch = function (args) { + return args; + }; + + // Now batch the data + store.batch(data); + + assert.strictEqual(store.size, 1); + assert.deepStrictEqual(store.index, ["name", "age"]); + + const results = store.find({name: "John"}); + assert.strictEqual(results.length, 1); + }); + + describe("with array data", () => { + it("should populate store when data is an array", () => { + // Test the specific code path where data is an array + const initialData = [ + {id: "1", name: "Alice", age: 30}, + {id: "2", name: "Bob", age: 25}, + {id: "3", name: "Charlie", age: 35} + ]; + + // This triggers the array data handling in the haro factory function + const store = haro(initialData, { + index: ["name"], + key: "id" + }); + + assert.equal(store.size, 3, "Store should be populated with initial data"); + assert.ok(store.has("1"), "Should contain first record"); + assert.ok(store.has("2"), "Should contain second record"); + assert.ok(store.has("3"), "Should contain third record"); + + // Verify indexing worked + const aliceResults = store.find({name: "Alice"}); + assert.equal(aliceResults.length, 1); + // Results are [key, record] pairs + assert.equal(aliceResults[0][1].age, 30); + }); + + it("should work with empty array data", () => { + const store = haro([], {index: ["name"]}); + assert.equal(store.size, 0, "Store should be empty when initialized with empty array"); + }); + + it("should work with null data (no array processing)", () => { + const store = haro(null, {index: ["name"]}); + assert.equal(store.size, 0, "Store should be empty when initialized with null"); + }); + }); +}); diff --git a/tests/unit/immutable.test.js b/tests/unit/immutable.test.js new file mode 100644 index 00000000..9c86b926 --- /dev/null +++ b/tests/unit/immutable.test.js @@ -0,0 +1,119 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Immutable Mode", () => { + let immutableStore; + + beforeEach(() => { + immutableStore = new Haro({immutable: true}); + }); + + it("should return frozen objects from get()", () => { + immutableStore.set("user1", {id: "user1", name: "John"}); + const result = immutableStore.get("user1"); + + assert.strictEqual(Object.isFrozen(result), true); + assert.strictEqual(Object.isFrozen(result[1]), true); + }); + + it("should return frozen arrays from find()", () => { + immutableStore.set("user1", {id: "user1", name: "John"}); + const results = immutableStore.find({name: "John"}); + + assert.strictEqual(Object.isFrozen(results), true); + }); + + it("should return frozen arrays from toArray()", () => { + immutableStore.set("user1", {id: "user1", name: "John"}); + const results = immutableStore.toArray(); + + assert.strictEqual(Object.isFrozen(results), true); + assert.strictEqual(Object.isFrozen(results[0]), true); + }); + + describe("find() method with immutable mode", () => { + it("should return frozen array when immutable=true", () => { + const store = new Haro({ + index: ["name"], + immutable: true + }); + + store.set("1", {id: "1", name: "Alice", age: 30}); + store.set("2", {id: "2", name: "Bob", age: 25}); + + const results = store.find({name: "Alice"}); + assert.ok(Object.isFrozen(results), "Results array should be frozen in immutable mode"); + assert.equal(results.length, 1); + // Results are [key, record] pairs when not using raw=true + assert.equal(results[0][1].name, "Alice"); + }); + + it("should return frozen array with raw=false explicitly", () => { + const store = new Haro({ + index: ["category"], + immutable: true + }); + + store.set("item1", {id: "item1", category: "books", title: "Book 1"}); + store.set("item2", {id: "item2", category: "books", title: "Book 2"}); + + // Call find with explicit false for raw parameter to ensure !raw is true + const results = store.find({category: "books"}, false); + + // Verify the array is frozen + assert.ok(Object.isFrozen(results), "Results array must be frozen"); + assert.equal(results.length, 2); + }); + + it("should test both raw conditions for branch coverage", () => { + const store = new Haro({ + index: ["type"], + immutable: true + }); + + store.set("1", {id: "1", type: "test"}); + + // Test raw=false with immutable=true (should freeze) + const frozenResults = store.find({type: "test"}, false); + assert.ok(Object.isFrozen(frozenResults), "Should be frozen when raw=false and immutable=true"); + + // Test raw=true with immutable=true (should NOT freeze) + const unfrozenResults = store.find({type: "test"}, true); + assert.ok(!Object.isFrozen(unfrozenResults), "Should NOT be frozen when raw=true"); + }); + }); + + describe("limit() method with immutable mode", () => { + it("should return frozen array when immutable=true", () => { + const store = new Haro({ + immutable: true + }); + + store.set("1", {id: "1", name: "Alice", age: 30}); + store.set("2", {id: "2", name: "Bob", age: 25}); + store.set("3", {id: "3", name: "Charlie", age: 35}); + + // Call limit() to trigger the immutable mode lines + const results = store.limit(0, 2); + assert.ok(Object.isFrozen(results), "Results should be frozen in immutable mode"); + assert.equal(results.length, 2, "Should return limited results"); + }); + }); + + describe("map() method with immutable mode", () => { + it("should return frozen array when immutable=true", () => { + const store = new Haro({ + immutable: true + }); + + store.set("1", {id: "1", name: "Alice", age: 30}); + store.set("2", {id: "2", name: "Bob", age: 25}); + + // Call map() without raw flag to trigger immutable mode lines + const results = store.map(record => ({...record, processed: true})); + assert.ok(Object.isFrozen(results), "Results should be frozen in immutable mode"); + assert.equal(results.length, 2, "Should return mapped results"); + }); + }); +}); diff --git a/tests/unit/import-export.test.js b/tests/unit/import-export.test.js new file mode 100644 index 00000000..5e57f296 --- /dev/null +++ b/tests/unit/import-export.test.js @@ -0,0 +1,69 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Data Import/Export", () => { + let store; + + beforeEach(() => { + store = new Haro(); + store.set("user1", {id: "user1", name: "John"}); + store.set("user2", {id: "user2", name: "Jane"}); + }); + + describe("dump()", () => { + it("should dump records by default", () => { + const data = store.dump(); + assert.strictEqual(data.length, 2); + assert.strictEqual(data[0][0], "user1"); + assert.strictEqual(data[0][1].name, "John"); + }); + + it("should dump records explicitly", () => { + const data = store.dump("records"); + assert.strictEqual(data.length, 2); + }); + + it("should dump indexes", () => { + const indexedStore = new Haro({index: ["name"]}); + indexedStore.set("user1", {id: "user1", name: "John"}); + const data = indexedStore.dump("indexes"); + + assert.strictEqual(Array.isArray(data), true); + assert.strictEqual(data.length, 1); + assert.strictEqual(data[0][0], "name"); + }); + }); + + describe("override()", () => { + it("should override records", () => { + const newData = [ + ["user3", {id: "user3", name: "Bob"}], + ["user4", {id: "user4", name: "Alice"}] + ]; + const result = store.override(newData, "records"); + + assert.strictEqual(result, true); + assert.strictEqual(store.size, 2); + assert.strictEqual(store.has("user1"), false); + assert.strictEqual(store.has("user3"), true); + }); + + it("should override indexes", () => { + const indexedStore = new Haro({index: ["name"]}); + const indexData = [ + ["name", [["John", ["user1"]], ["Jane", ["user2"]]]] + ]; + const result = indexedStore.override(indexData, "indexes"); + + assert.strictEqual(result, true); + assert.strictEqual(indexedStore.indexes.size, 1); + }); + + it("should throw error for invalid type", () => { + assert.throws(() => { + store.override([], "invalid"); + }, /Invalid type/); + }); + }); +}); diff --git a/tests/unit/indexing.test.js b/tests/unit/indexing.test.js new file mode 100644 index 00000000..83cb9557 --- /dev/null +++ b/tests/unit/indexing.test.js @@ -0,0 +1,176 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Indexing", () => { + let indexedStore; + + beforeEach(() => { + indexedStore = new Haro({ + index: ["name", "age", "department", "name|department", "age|department", "department|name"] + }); + }); + + describe("find()", () => { + beforeEach(() => { + indexedStore.set("user1", {id: "user1", name: "John", age: 30, department: "IT"}); + indexedStore.set("user2", {id: "user2", name: "Jane", age: 25, department: "HR"}); + indexedStore.set("user3", {id: "user3", name: "Bob", age: 30, department: "IT"}); + }); + + it("should find records by single field", () => { + const results = indexedStore.find({name: "John"}); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0][1].name, "John"); + }); + + it("should find records by multiple fields", () => { + const results = indexedStore.find({age: 30, department: "IT"}); + assert.strictEqual(results.length, 2); + }); + + it("should find records using composite index", () => { + const results = indexedStore.find({name: "John", department: "IT"}); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0][1].name, "John"); + }); + + it("should find records using composite index with out-of-order predicates", () => { + // Fields are sorted alphabetically, so both orderings should work + const results1 = indexedStore.find({name: "John", department: "IT"}); + const results2 = indexedStore.find({department: "IT", name: "John"}); + + assert.strictEqual(results1.length, 1); + assert.strictEqual(results2.length, 1); + assert.strictEqual(results1[0][1].name, "John"); + assert.strictEqual(results2[0][1].name, "John"); + + // Should find the same record + assert.strictEqual(results1[0][0], results2[0][0]); + }); + + it("should work with three-field composite index regardless of predicate order", () => { + // Add a store with a three-field composite index + const tripleStore = new Haro({ + index: ["name", "age", "department", "age|department|name"] + }); + + tripleStore.set("user1", {id: "user1", name: "John", age: 30, department: "IT"}); + tripleStore.set("user2", {id: "user2", name: "Jane", age: 25, department: "HR"}); + + // All these should find the same record because keys are sorted alphabetically + const results1 = tripleStore.find({name: "John", age: 30, department: "IT"}); + const results2 = tripleStore.find({department: "IT", name: "John", age: 30}); + const results3 = tripleStore.find({age: 30, department: "IT", name: "John"}); + + assert.strictEqual(results1.length, 1); + assert.strictEqual(results2.length, 1); + assert.strictEqual(results3.length, 1); + + // All should find the same record + assert.strictEqual(results1[0][0], results2[0][0]); + assert.strictEqual(results2[0][0], results3[0][0]); + assert.strictEqual(results1[0][1].name, "John"); + }); + + it("should return empty array when no matches found", () => { + const results = indexedStore.find({name: "NonExistent"}); + assert.strictEqual(results.length, 0); + }); + + it("should return frozen results in immutable mode", () => { + const immutableStore = new Haro({ + index: ["name"], + immutable: true + }); + immutableStore.set("user1", {id: "user1", name: "John"}); + const results = immutableStore.find({name: "John"}); + + assert.strictEqual(Object.isFrozen(results), true); + }); + }); + + describe("setIndex()", () => { + it("should create new index when it doesn't exist", () => { + const store = new Haro({ + index: ["name"] + }); + + // Add data first + store.set("1", {name: "Alice", age: 30}); + + // Now manually call setIndex to trigger index creation for new field + store.setIndex("1", {category: "admin"}, "category"); + + // Verify the new index was created + assert.ok(store.indexes.has("category"), "New index should be created"); + const categoryIndex = store.indexes.get("category"); + assert.ok(categoryIndex.has("admin"), "Index should contain the value"); + assert.ok(categoryIndex.get("admin").has("1"), "Index should map value to key"); + }); + + it("should handle array values in index creation", () => { + const store = new Haro({ + index: ["tags"] + }); + + // This will trigger the index creation path for array values + store.set("1", {name: "Alice", tags: ["developer", "admin"]}); + + const tagsIndex = store.indexes.get("tags"); + assert.ok(tagsIndex.has("developer"), "Index should contain array element"); + assert.ok(tagsIndex.has("admin"), "Index should contain array element"); + }); + }); + + describe("reindex()", () => { + it("should rebuild all indexes", () => { + indexedStore.set("user1", {id: "user1", name: "John", age: 30}); + indexedStore.indexes.clear(); // Simulate corrupted indexes + + indexedStore.reindex(); + const results = indexedStore.find({name: "John"}); + assert.strictEqual(results.length, 1); + }); + + it("should add new index field", () => { + indexedStore.set("user1", {id: "user1", name: "John", email: "john@example.com"}); + indexedStore.reindex("email"); + + const results = indexedStore.find({email: "john@example.com"}); + assert.strictEqual(results.length, 1); + assert.strictEqual(indexedStore.index.includes("email"), true); + }); + }); + + describe("indexKeys()", () => { + it("should generate keys for composite index", () => { + const data = {name: "John", department: "IT"}; + const keys = indexedStore.indexKeys("name|department", "|", data); + assert.deepStrictEqual(keys, ["IT|John"]); + }); + + it("should handle array values in composite index", () => { + const data = {name: "John", tags: ["admin", "user"]}; + const keys = indexedStore.indexKeys("name|tags", "|", data); + assert.deepStrictEqual(keys, ["John|admin", "John|user"]); + }); + + it("should handle empty field values", () => { + const data = {name: "John", department: undefined}; + const keys = indexedStore.indexKeys("name|department", "|", data); + assert.deepStrictEqual(keys, ["undefined|John"]); + }); + + it("should sort composite index fields alphabetically", () => { + const data = {name: "John", department: "IT"}; + + // Both should produce the same keys because fields are sorted alphabetically + const keys1 = indexedStore.indexKeys("name|department", "|", data); + const keys2 = indexedStore.indexKeys("department|name", "|", data); + + assert.deepStrictEqual(keys1, ["IT|John"]); + assert.deepStrictEqual(keys2, ["IT|John"]); + }); + }); +}); diff --git a/tests/unit/lifecycle.test.js b/tests/unit/lifecycle.test.js new file mode 100644 index 00000000..9368b377 --- /dev/null +++ b/tests/unit/lifecycle.test.js @@ -0,0 +1,124 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Lifecycle Hooks", () => { + class TestStore extends Haro { + constructor (config) { + super(config); + this.hooks = { + beforeBatch: [], + beforeClear: [], + beforeDelete: [], + beforeSet: [], + onbatch: [], + onclear: [], + ondelete: [], + onoverride: [], + onset: [] + }; + } + + beforeBatch (args, type) { + this.hooks.beforeBatch.push({args, type}); + + return args; + } + + beforeClear () { + this.hooks.beforeClear.push(true); + + return super.beforeClear(); + } + + beforeDelete (key, batch) { + this.hooks.beforeDelete.push({key, batch}); + + return super.beforeDelete(key, batch); + } + + beforeSet (key, data, batch, override) { + this.hooks.beforeSet.push({key, data, batch, override}); + + return super.beforeSet(key, data, batch, override); + } + + onbatch (result, type) { + this.hooks.onbatch.push({result, type}); + + return super.onbatch(result, type); + } + + onclear () { + this.hooks.onclear.push(true); + + return super.onclear(); + } + + ondelete (key, batch) { + this.hooks.ondelete.push({key, batch}); + + return super.ondelete(key, batch); + } + + onoverride (type) { + this.hooks.onoverride.push({type}); + + return super.onoverride(type); + } + + onset (result, batch) { + this.hooks.onset.push({result, batch}); + + return super.onset(result, batch); + } + } + + let testStore; + + beforeEach(() => { + testStore = new TestStore(); + }); + + it("should call beforeSet and onset hooks", () => { + testStore.set("user1", {id: "user1", name: "John"}); + + assert.strictEqual(testStore.hooks.beforeSet.length, 1); + assert.strictEqual(testStore.hooks.onset.length, 1); + assert.strictEqual(testStore.hooks.beforeSet[0].key, "user1"); + assert.strictEqual(testStore.hooks.onset[0].result[1].name, "John"); + }); + + it("should call beforeDelete and ondelete hooks", () => { + testStore.set("user1", {id: "user1", name: "John"}); + testStore.delete("user1"); + + assert.strictEqual(testStore.hooks.beforeDelete.length, 1); + assert.strictEqual(testStore.hooks.ondelete.length, 1); + assert.strictEqual(testStore.hooks.beforeDelete[0].key, "user1"); + }); + + it("should call beforeClear and onclear hooks", () => { + testStore.set("user1", {id: "user1", name: "John"}); + testStore.clear(); + + assert.strictEqual(testStore.hooks.beforeClear.length, 1); + assert.strictEqual(testStore.hooks.onclear.length, 1); + }); + + it("should call beforeBatch and onbatch hooks", () => { + const data = [{id: "user1", name: "John"}]; + testStore.batch(data); + + assert.strictEqual(testStore.hooks.beforeBatch.length, 1); + assert.strictEqual(testStore.hooks.onbatch.length, 1); + }); + + it("should call onoverride hook", () => { + const data = [["user1", {id: "user1", name: "John"}]]; + testStore.override(data, "records"); + + assert.strictEqual(testStore.hooks.onoverride.length, 1); + assert.strictEqual(testStore.hooks.onoverride[0].type, "records"); + }); +}); diff --git a/tests/unit/properties.test.js b/tests/unit/properties.test.js new file mode 100644 index 00000000..05304fb4 --- /dev/null +++ b/tests/unit/properties.test.js @@ -0,0 +1,33 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Properties", () => { + let store; + + beforeEach(() => { + store = new Haro(); + }); + + it("should have correct size property", () => { + assert.strictEqual(store.size, 0); + store.set("user1", {id: "user1", name: "John"}); + assert.strictEqual(store.size, 1); + }); + + it("should have correct registry property", () => { + assert.deepStrictEqual(store.registry, []); + store.set("user1", {id: "user1", name: "John"}); + assert.deepStrictEqual(store.registry, ["user1"]); + }); + + it("should update registry when records are added/removed", () => { + store.set("user1", {id: "user1", name: "John"}); + store.set("user2", {id: "user2", name: "Jane"}); + assert.strictEqual(store.registry.length, 2); + + store.delete("user1"); + assert.strictEqual(store.registry.length, 1); + assert.strictEqual(store.registry[0], "user2"); + }); +}); diff --git a/tests/unit/search.test.js b/tests/unit/search.test.js new file mode 100644 index 00000000..74d3aeaf --- /dev/null +++ b/tests/unit/search.test.js @@ -0,0 +1,339 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Searching and Filtering", () => { + let store; + + beforeEach(() => { + store = new Haro({index: ["name", "age", "tags"]}); + store.set("user1", {id: "user1", name: "John", age: 30, tags: ["admin", "user"]}); + store.set("user2", {id: "user2", name: "Jane", age: 25, tags: ["user"]}); + store.set("user3", {id: "user3", name: "Bob", age: 35, tags: ["admin"]}); + }); + + describe("search()", () => { + it("should search by exact value", () => { + const results = store.search("John"); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0][1].name, "John"); + }); + + it("should search in specific index", () => { + const results = store.search("John", "name"); + assert.strictEqual(results.length, 1); + }); + + it("should search in multiple indexes", () => { + const results = store.search("admin", ["tags"]); + assert.strictEqual(results.length, 2); + }); + + it("should search with regex", () => { + const results = store.search(/^J/, "name"); + assert.strictEqual(results.length, 2); + }); + + it("should search with function", () => { + const results = store.search(value => value.includes("o"), "name"); + assert.strictEqual(results.length, 2); // John and Bob + }); + + it("should return empty array for null/undefined value", () => { + const results = store.search(null); + assert.strictEqual(results.length, 0); + }); + + it("should return frozen results in immutable mode with raw=false", () => { + const immutableStore = new Haro({ + index: ["name", "tags"], + immutable: true + }); + + immutableStore.set("user1", {id: "user1", name: "Alice", tags: ["admin"]}); + immutableStore.set("user2", {id: "user2", name: "Bob", tags: ["user"]}); + + // Call search with raw=false (default) and immutable=true to cover lines 695-696 + const results = immutableStore.search("Alice", "name", false); + assert.strictEqual(Object.isFrozen(results), true, "Search results should be frozen in immutable mode"); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0][1].name, "Alice"); + }); + }); + + describe("filter()", () => { + it("should filter records with predicate function", () => { + const results = store.filter(record => record.age > 25); + assert.strictEqual(results.length, 2); + }); + + it("should throw error for non-function predicate", () => { + assert.throws(() => { + store.filter("not a function"); + }, /Invalid function/); + }); + + it("should return frozen results in immutable mode", () => { + const immutableStore = new Haro({immutable: true}); + immutableStore.set("user1", {id: "user1", age: 30}); + const results = immutableStore.filter(record => record.age > 25); + + assert.strictEqual(Object.isFrozen(results), true); + }); + }); + + describe("where()", () => { + it("should filter with predicate object", () => { + const results = store.where({age: 30}); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].name, "John"); + }); + + it("should filter with array predicate using OR logic", () => { + const results = store.where({tags: ["admin", "user"]}, "||"); + assert.strictEqual(results.length, 3); // All users have either admin or user tag + }); + + it("should filter with array predicate using AND logic", () => { + const results = store.where({tags: ["admin", "user"]}, "&&"); + assert.strictEqual(results.length, 1); // Only John has both tags + }); + + it("should filter with regex predicate", () => { + const results = store.where({name: /^J/}); + assert.strictEqual(results.length, 0); + }); + + it("should return empty array for non-indexed fields", () => { + const results = store.where({nonIndexedField: "value"}); + assert.strictEqual(results.length, 0); + }); + + describe("indexed query optimization", () => { + it("should use indexed query optimization for multiple indexed fields", () => { + const optimizedStore = new Haro({ + index: ["category", "status", "priority"] + }); + + // Add data + optimizedStore.set("1", {category: "bug", status: "open", priority: "high"}); + optimizedStore.set("2", {category: "bug", status: "closed", priority: "low"}); + optimizedStore.set("3", {category: "feature", status: "open", priority: "high"}); + optimizedStore.set("4", {category: "bug", status: "open", priority: "medium"}); + + // Query with multiple indexed fields to trigger indexed optimization + const results = optimizedStore.where({ + category: "bug", + status: "open" + }, "&&"); + + assert.equal(results.length, 2, "Should find records matching both criteria"); + assert.ok(results.every(r => r.category === "bug" && r.status === "open")); + }); + + it("should handle array predicates in indexed query", () => { + const arrayStore = new Haro({ + index: ["category", "tags"] + }); + + // Add data + arrayStore.set("1", {id: "1", category: "tech", tags: ["javascript", "nodejs"]}); + arrayStore.set("2", {id: "2", category: "tech", tags: ["python", "django"]}); + arrayStore.set("3", {id: "3", category: "business", tags: ["javascript", "react"]}); + + // Query with array predicate on indexed field + const results = arrayStore.where({ + category: ["tech"] + }, "&&"); + + assert.equal(results.length, 2, "Should find records matching array predicate"); + assert.ok(results.every(r => r.category === "tech")); + }); + }); + + describe("fallback to full scan", () => { + it("should fallback to full scan when no indexed fields are available", () => { + const fallbackStore = new Haro({ + index: ["name"] // Only index 'name' field + }); + + // Add data + fallbackStore.set("1", {id: "1", name: "Alice", age: 30, category: "admin"}); + fallbackStore.set("2", {id: "2", name: "Bob", age: 25, category: "user"}); + fallbackStore.set("3", {id: "3", name: "Charlie", age: 35, category: "admin"}); + + // Query for non-existent value + const results = fallbackStore.where({ + name: "nonexistent" + }, "&&"); + + assert.equal(results.length, 0, "Should return empty array when no matches"); + }); + + it("should trigger true fallback to full scan", () => { + const scanStore = new Haro({ + index: ["age"] + }); + + scanStore.set("1", {id: "1", name: "Alice", age: 30, category: "admin"}); + scanStore.set("2", {id: "2", name: "Bob", age: 25, category: "user"}); + + // Remove the age index to force fallback + scanStore.indexes.delete("age"); + + // Test that the method works + const results = scanStore.where({age: 30}, "&&"); + assert.equal(Array.isArray(results), true, "Should return an array"); + }); + + it("should return empty array when no matches in fallback scan", () => { + const emptyStore = new Haro({ + index: ["name"] + }); + + emptyStore.set("1", {id: "1", name: "Alice", age: 30}); + emptyStore.set("2", {id: "2", name: "Bob", age: 25}); + + // Query that won't match anything + const results = emptyStore.where({ + age: 40, + category: "nonexistent" + }, "&&"); + + assert.equal(results.length, 0, "Should return empty array when no matches"); + }); + }); + }); + + describe("sortBy()", () => { + it("should sort by indexed field", () => { + const results = store.sortBy("name"); + assert.strictEqual(results[0][1].name, "Bob"); + assert.strictEqual(results[1][1].name, "Jane"); + assert.strictEqual(results[2][1].name, "John"); + }); + + it("should throw error for empty field", () => { + assert.throws(() => { + store.sortBy(""); + }, /Invalid field/); + }); + + it("should create index if not exists", () => { + const results = store.sortBy("name"); + assert.strictEqual(results[0][1].name, "Bob"); + assert.strictEqual(results[1][1].name, "Jane"); + assert.strictEqual(results[2][1].name, "John"); + }); + + describe("with reindexing and immutable mode", () => { + it("should reindex field if not exists and return frozen results", () => { + const immutableStore = new Haro({ + immutable: true + }); + + immutableStore.set("1", {id: "1", name: "Charlie", age: 35}); + immutableStore.set("2", {id: "2", name: "Alice", age: 30}); + immutableStore.set("3", {id: "3", name: "Bob", age: 25}); + + // sortBy on non-indexed field will trigger reindex + const results = immutableStore.sortBy("age"); + + // Verify reindexing happened + assert.ok(immutableStore.indexes.has("age"), "Index should be created during sortBy"); + + // Verify results are frozen + assert.ok(Object.isFrozen(results), "Results should be frozen in immutable mode"); + + // Verify sorting worked - results are [key, record] pairs + assert.equal(results[0][1].age, 25); + assert.equal(results[1][1].age, 30); + assert.equal(results[2][1].age, 35); + }); + }); + }); + + describe("matchesPredicate() complex array logic", () => { + it("should handle array predicate with array value using AND logic", () => { + const testStore = new Haro(); + const record = {tags: ["javascript", "nodejs", "react"]}; + + // Test array predicate with array value using AND (every) + const result = testStore.matchesPredicate(record, {tags: ["javascript", "nodejs"]}, "&&"); + assert.equal(result, true, "Should match when all predicate values are in record array"); + + const result2 = testStore.matchesPredicate(record, {tags: ["javascript", "python"]}, "&&"); + assert.equal(result2, false, "Should not match when not all predicate values are in record array"); + }); + + it("should handle array predicate with array value using OR logic", () => { + const testStore = new Haro(); + const record = {tags: ["javascript", "nodejs"]}; + + // Test array predicate with array value using OR (some) + const result = testStore.matchesPredicate(record, {tags: ["python", "nodejs"]}, "||"); + assert.equal(result, true, "Should match when at least one predicate value is in record array"); + }); + + it("should handle array predicate with scalar value using AND logic", () => { + const testStore = new Haro(); + const record = {category: "tech"}; + + // Test array predicate with scalar value using AND (every) + const result = testStore.matchesPredicate(record, {category: ["tech"]}, "&&"); + assert.equal(result, true, "Should match when predicate array contains the scalar value"); + + const result2 = testStore.matchesPredicate(record, {category: ["business", "finance"]}, "&&"); + assert.equal(result2, false, "Should not match when predicate array doesn't contain scalar value"); + }); + + it("should handle array predicate with scalar value using OR logic", () => { + const testStore = new Haro(); + const record = {category: "tech"}; + + // Test array predicate with scalar value using OR (some) + const result = testStore.matchesPredicate(record, {category: ["business", "tech"]}, "||"); + assert.equal(result, true, "Should match when predicate array contains the scalar value"); + }); + + it("should handle regex predicate with array value using AND logic", () => { + const testStore = new Haro(); + const record = {tags: ["reactjs", "vuejs", "angularjs"]}; + + // Test regex predicate with array value using AND (every) + const result = testStore.matchesPredicate(record, {tags: /js$/}, "&&"); + assert.equal(result, true, "Should match when regex matches all array values"); + + const record2 = {tags: ["javascript", "nodejs", "reactjs"]}; + const result2 = testStore.matchesPredicate(record2, {tags: /js$/}, "&&"); + assert.equal(result2, false, "Should not match when regex doesn't match all array values"); + }); + + it("should handle regex predicate with array value using OR logic", () => { + const testStore = new Haro(); + const record = {tags: ["python", "nodejs", "java"]}; + + // Test regex predicate with array value using OR (some) + const result = testStore.matchesPredicate(record, {tags: /^node/}, "||"); + assert.equal(result, true, "Should match when regex matches at least one array value"); + }); + + it("should handle regex predicate with scalar value", () => { + const testStore = new Haro(); + const record = {name: "javascript"}; + + // Test regex predicate with scalar value + const result = testStore.matchesPredicate(record, {name: /script$/}, "&&"); + assert.equal(result, true, "Should match when regex matches scalar value"); + }); + + it("should handle array value with scalar predicate", () => { + const testStore = new Haro(); + const record = {tags: ["javascript"]}; + + // Test the specific edge case for array values with non-array predicate + const result = testStore.matchesPredicate(record, {tags: "javascript"}, "&&"); + assert.equal(result, true, "Should handle array value with scalar predicate"); + }); + }); +}); diff --git a/tests/unit/utilities.test.js b/tests/unit/utilities.test.js new file mode 100644 index 00000000..562ec378 --- /dev/null +++ b/tests/unit/utilities.test.js @@ -0,0 +1,375 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Utility Methods", () => { + let store; + + beforeEach(() => { + store = new Haro(); + }); + + describe("clone()", () => { + it("should create deep clone of object", () => { + const original = {name: "John", tags: ["admin", "user"]}; + const cloned = store.clone(original); + + cloned.tags.push("new"); + assert.strictEqual(original.tags.length, 2); + assert.strictEqual(cloned.tags.length, 3); + }); + + it("should clone primitives", () => { + assert.strictEqual(store.clone("string"), "string"); + assert.strictEqual(store.clone(123), 123); + assert.strictEqual(store.clone(true), true); + }); + }); + + describe("each()", () => { + it("should iterate over array with callback", () => { + const items = ["a", "b", "c"]; + const results = []; + + store.each(items, (item, index) => { + results.push(`${index}:${item}`); + }); + + assert.deepStrictEqual(results, ["0:a", "1:b", "2:c"]); + }); + + it("should handle empty array", () => { + const results = []; + store.each([], () => results.push("called")); + assert.strictEqual(results.length, 0); + }); + }); + + describe("forEach()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", name: "John"}); + store.set("user2", {id: "user2", name: "Jane"}); + }); + + it("should iterate over all records", () => { + const results = []; + store.forEach((value, key) => { + results.push(`${key}:${value.name}`); + }); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results.includes("user1:John"), true); + assert.strictEqual(results.includes("user2:Jane"), true); + }); + }); + + describe("map()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", name: "John", age: 30}); + store.set("user2", {id: "user2", name: "Jane", age: 25}); + }); + + it("should transform all records", () => { + const results = store.map(record => record.name); + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0][1], "John"); + assert.strictEqual(results[1][1], "Jane"); + }); + + it("should throw error for non-function mapper", () => { + assert.throws(() => { + store.map("not a function"); + }, /Invalid function/); + }); + }); + + describe("reduce()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", age: 30}); + store.set("user2", {id: "user2", age: 25}); + }); + + it("should reduce all records to single value", () => { + const totalAge = store.reduce((sum, record) => sum + record.age, 0); + assert.strictEqual(totalAge, 55); + }); + + it("should use default accumulator", () => { + const names = store.reduce((acc, record) => { + acc.push(record.id); + + return acc; + }); + assert.deepStrictEqual(names, ["user1", "user2"]); + }); + }); + + describe("merge()", () => { + it("should merge objects", () => { + const a = {x: 1, y: 2}; + const b = {y: 3, z: 4}; + const result = store.merge(a, b); + + assert.deepStrictEqual(result, {x: 1, y: 3, z: 4}); + }); + + it("should concatenate arrays", () => { + const a = [1, 2]; + const b = [3, 4]; + const result = store.merge(a, b); + + assert.deepStrictEqual(result, [1, 2, 3, 4]); + }); + + it("should override arrays when override is true", () => { + const a = [1, 2]; + const b = [3, 4]; + const result = store.merge(a, b, true); + + assert.deepStrictEqual(result, [3, 4]); + }); + + it("should replace primitives", () => { + const result = store.merge("old", "new"); + assert.strictEqual(result, "new"); + }); + }); + + describe("uuid()", () => { + it("should generate valid UUID", () => { + const id = store.uuid(); + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + assert.strictEqual(uuidRegex.test(id), true); + }); + + it("should generate unique UUIDs", () => { + const id1 = store.uuid(); + const id2 = store.uuid(); + assert.notStrictEqual(id1, id2); + }); + }); + + describe("freeze()", () => { + it("should freeze multiple arguments", () => { + const obj1 = {a: 1}; + const obj2 = {b: 2}; + const result = store.freeze(obj1, obj2); + + assert.strictEqual(Object.isFrozen(result), true); + assert.strictEqual(Object.isFrozen(result[0]), true); + assert.strictEqual(Object.isFrozen(result[1]), true); + }); + }); + + describe("list()", () => { + it("should convert record to [key, value] format", () => { + const record = {id: "user1", name: "John"}; + const result = store.list(record); + + assert.deepStrictEqual(result, ["user1", record]); + }); + + it("should freeze result in immutable mode", () => { + const immutableStore = new Haro({immutable: true}); + const record = {id: "user1", name: "John"}; + const result = immutableStore.list(record); + + assert.strictEqual(Object.isFrozen(result), true); + }); + }); + + describe("limit()", () => { + beforeEach(() => { + for (let i = 0; i < 10; i++) { + store.set(`user${i}`, {id: `user${i}`, name: `User${i}`}); + } + }); + + it("should return limited subset of records", () => { + const results = store.limit(0, 5); + assert.strictEqual(results.length, 5); + }); + + it("should support offset", () => { + const results = store.limit(5, 3); + assert.strictEqual(results.length, 3); + assert.strictEqual(results[0][0], "user5"); + }); + + it("should handle offset beyond data size", () => { + const results = store.limit(20, 5); + assert.strictEqual(results.length, 0); + }); + }); + + describe("sort()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", name: "Charlie", age: 30}); + store.set("user2", {id: "user2", name: "Alice", age: 25}); + store.set("user3", {id: "user3", name: "Bob", age: 35}); + }); + + it("should sort records with comparator function", () => { + const results = store.sort((a, b) => a.name.localeCompare(b.name)); + assert.strictEqual(results[0].name, "Alice"); + assert.strictEqual(results[1].name, "Bob"); + assert.strictEqual(results[2].name, "Charlie"); + }); + + it("should return frozen results when frozen=true", () => { + const results = store.sort((a, b) => a.age - b.age, true); + assert.strictEqual(Object.isFrozen(results), true); + }); + }); + + describe("toArray()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", name: "John"}); + store.set("user2", {id: "user2", name: "Jane"}); + }); + + it("should convert store to array", () => { + const results = store.toArray(); + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].name, "John"); + assert.strictEqual(results[1].name, "Jane"); + }); + + it("should return frozen array in immutable mode", () => { + const immutableStore = new Haro({immutable: true}); + immutableStore.set("user1", {id: "user1", name: "John"}); + const results = immutableStore.toArray(); + + assert.strictEqual(Object.isFrozen(results), true); + assert.strictEqual(Object.isFrozen(results[0]), true); + }); + }); + + describe("entries(), keys(), values()", () => { + beforeEach(() => { + store.set("user1", {id: "user1", name: "John"}); + store.set("user2", {id: "user2", name: "Jane"}); + }); + + it("should return entries iterator", () => { + const entries = Array.from(store.entries()); + assert.strictEqual(entries.length, 2); + assert.strictEqual(entries[0][0], "user1"); + assert.strictEqual(entries[0][1].name, "John"); + }); + + it("should return keys iterator", () => { + const keys = Array.from(store.keys()); + assert.strictEqual(keys.length, 2); + assert.strictEqual(keys.includes("user1"), true); + assert.strictEqual(keys.includes("user2"), true); + }); + + it("should return values iterator", () => { + const values = Array.from(store.values()); + assert.strictEqual(values.length, 2); + assert.strictEqual(values[0].name, "John"); + assert.strictEqual(values[1].name, "Jane"); + }); + }); + + describe("sortKeys()", () => { + it("should sort strings using localeCompare", () => { + const result = store.sortKeys("apple", "banana"); + assert.strictEqual(result < 0, true, "apple should come before banana"); + + const result2 = store.sortKeys("zebra", "apple"); + assert.strictEqual(result2 > 0, true, "zebra should come after apple"); + + const result3 = store.sortKeys("same", "same"); + assert.strictEqual(result3, 0, "identical strings should return 0"); + }); + + it("should sort numbers using numeric comparison", () => { + const result = store.sortKeys(5, 10); + assert.strictEqual(result, -5, "5 should come before 10"); + + const result2 = store.sortKeys(20, 3); + assert.strictEqual(result2, 17, "20 should come after 3"); + + const result3 = store.sortKeys(7, 7); + assert.strictEqual(result3, 0, "identical numbers should return 0"); + }); + + it("should handle negative numbers correctly", () => { + const result = store.sortKeys(-5, 3); + assert.strictEqual(result, -8, "-5 should come before 3"); + + const result2 = store.sortKeys(-10, -2); + assert.strictEqual(result2, -8, "-10 should come before -2"); + }); + + it("should handle floating point numbers", () => { + const result = store.sortKeys(3.14, 2.71); + assert.strictEqual(result > 0, true, "3.14 should come after 2.71"); + assert.strictEqual(Math.abs(result - 0.43) < 0.01, true, "result should be approximately 0.43"); + + const result2 = store.sortKeys(1.5, 1.5); + assert.strictEqual(result2, 0, "identical floats should return 0"); + }); + + it("should convert mixed types to strings and sort", () => { + const result = store.sortKeys(10, "5"); + assert.strictEqual(result < 0, true, "number 10 as string should come before string '5'"); + + const result2 = store.sortKeys("abc", 123); + assert.strictEqual(result2 > 0, true, "string 'abc' should come after number 123 as string"); + }); + + it("should handle null and undefined values", () => { + const result = store.sortKeys(null, "test"); + assert.strictEqual(result < 0, true, "null should come before 'test'"); + + const result2 = store.sortKeys(undefined, "test"); + assert.strictEqual(result2 > 0, true, "undefined should come after 'test'"); + + const result3 = store.sortKeys(null, undefined); + assert.strictEqual(result3 < 0, true, "null should come before undefined"); + }); + + it("should handle boolean values", () => { + const result = store.sortKeys(true, false); + assert.strictEqual(result > 0, true, "true should come after false"); + + const result2 = store.sortKeys(false, "test"); + assert.strictEqual(result2 < 0, true, "false should come before 'test'"); + }); + + it("should handle objects by converting to string", () => { + const obj1 = {name: "test"}; + const obj2 = {value: 123}; + const result = store.sortKeys(obj1, obj2); + + // Objects get converted to "[object Object]" so they should be equal + assert.strictEqual(result, 0, "objects should be equal when converted to string"); + }); + + it("should work as Array.sort comparator", () => { + const mixed = ["zebra", "apple", "banana"]; + mixed.sort(store.sortKeys.bind(store)); + assert.deepStrictEqual(mixed, ["apple", "banana", "zebra"]); + + const numbers = [10, 3, 7, 1]; + numbers.sort(store.sortKeys.bind(store)); + assert.deepStrictEqual(numbers, [1, 3, 7, 10]); + + const mixedTypes = [5, "3", 1, "10"]; + mixedTypes.sort(store.sortKeys.bind(store)); + // When converted to strings: "1", "10", "3", "5" + assert.deepStrictEqual(mixedTypes, [1, "10", "3", 5]); + }); + + it("should handle special string characters", () => { + const result = store.sortKeys("café", "cafe"); + assert.strictEqual(typeof result, "number", "should return a number"); + + const result2 = store.sortKeys("ñ", "n"); + assert.strictEqual(typeof result2, "number", "should handle accented characters"); + }); + }); +}); diff --git a/tests/unit/versioning.test.js b/tests/unit/versioning.test.js new file mode 100644 index 00000000..a81e5c90 --- /dev/null +++ b/tests/unit/versioning.test.js @@ -0,0 +1,38 @@ +import assert from "node:assert"; +import {describe, it, beforeEach} from "mocha"; +import {Haro} from "../../src/haro.js"; + +describe("Versioning", () => { + let versionedStore; + + beforeEach(() => { + versionedStore = new Haro({versioning: true}); + }); + + it("should create version when updating record", () => { + versionedStore.set("user1", {id: "user1", name: "John", age: 30}); + versionedStore.set("user1", {id: "user1", name: "John", age: 31}); + + const versions = versionedStore.versions.get("user1"); + assert.strictEqual(versions.size, 1); + + const version = Array.from(versions)[0]; + assert.strictEqual(version.age, 30); + assert.strictEqual(Object.isFrozen(version), true); + }); + + it("should not create version for new record", () => { + versionedStore.set("user1", {id: "user1", name: "John"}); + + const versions = versionedStore.versions.get("user1"); + assert.strictEqual(versions.size, 0); + }); + + it("should delete versions when record is deleted", () => { + versionedStore.set("user1", {id: "user1", name: "John"}); + versionedStore.set("user1", {id: "user1", name: "John Updated"}); + versionedStore.delete("user1"); + + assert.strictEqual(versionedStore.versions.has("user1"), false); + }); +}); diff --git a/types/constants.d.ts b/types/constants.d.ts index 3fd85d96..6a7628f0 100644 --- a/types/constants.d.ts +++ b/types/constants.d.ts @@ -1,25 +1,28 @@ -export const STRING_COMMA: ","; -export const STRING_EMPTY: ""; -export const STRING_PIPE: "|"; -export const STRING_DOUBLE_PIPE: "||"; -export const STRING_A: "a"; -export const STRING_B: "b"; -export const STRING_DEL: "del"; -export const STRING_FUNCTION: "function"; -export const STRING_INDEXES: "indexes"; -export const STRING_INVALID_FIELD: "Invalid field"; -export const STRING_INVALID_FUNCTION: "Invalid function"; -export const STRING_INVALID_TYPE: "Invalid type"; -export const STRING_OBJECT: "object"; -export const STRING_RECORD_NOT_FOUND: "Record not found"; -export const STRING_RECORDS: "records"; -export const STRING_REGISTRY: "registry"; -export const STRING_SET: "set"; -export const STRING_SIZE: "size"; -export const INT_0: 0; -export const INT_1: 1; -export const INT_3: 3; -export const INT_4: 4; -export const INT_8: 8; -export const INT_9: 9; -export const INT_16: 16; +// String constants - Single characters and symbols +export const STRING_COMMA: string; +export const STRING_EMPTY: string; +export const STRING_PIPE: string; +export const STRING_DOUBLE_PIPE: string; +export const STRING_DOUBLE_AND: string; + +// String constants - Operation and type names +export const STRING_ID: string; +export const STRING_DEL: string; +export const STRING_FUNCTION: string; +export const STRING_INDEXES: string; +export const STRING_OBJECT: string; +export const STRING_RECORDS: string; +export const STRING_REGISTRY: string; +export const STRING_SET: string; +export const STRING_SIZE: string; +export const STRING_STRING: string; +export const STRING_NUMBER: string; + +// String constants - Error messages +export const STRING_INVALID_FIELD: string; +export const STRING_INVALID_FUNCTION: string; +export const STRING_INVALID_TYPE: string; +export const STRING_RECORD_NOT_FOUND: string; + +// Integer constants +export const INT_0: number; diff --git a/types/haro.d.ts b/types/haro.d.ts index 6da18fd5..b04d0ba6 100644 --- a/types/haro.d.ts +++ b/types/haro.d.ts @@ -1,58 +1,368 @@ -export function haro(data?: any, config?: {}): Haro; +/** + * Configuration object for creating a Haro instance + */ +export interface HaroConfig { + delimiter?: string; + id?: string; + immutable?: boolean; + index?: string[]; + key?: string; + versioning?: boolean; +} + +/** + * Haro is a modern immutable DataStore for collections of records with indexing, + * versioning, and batch operations support. It provides a Map-like interface + * with advanced querying capabilities through indexes. + */ export class Haro { - constructor({ delimiter, id, index, key, versioning }?: { - delimiter?: string; - id?: any; - index?: any[]; - key?: string; - versioning?: boolean; - }); - data: any; - delimiter: string; - id: any; - index: any[]; - indexes: any; - key: string; - versions: any; - versioning: boolean; - batch(args: any, type?: string): any[]; - beforeBatch(arg: any, type?: string): any; - beforeClear(): void; - beforeDelete(key?: string, batch?: boolean): (string | boolean)[]; - beforeSet(key?: string, batch?: boolean): (string | boolean)[]; - clear(): this; - clone(arg: any): any; - del(key?: string, batch?: boolean): void; - delIndex(index: any, indexes: any, delimiter: any, key: any, data: any): void; - dump(type?: string): any; - each(arr: any[], fn: any): any[]; - entries(): any; - find(where?: {}, raw?: boolean): any[] | [any, any][]; - filter(fn: any, raw?: boolean): any[] | [any, any][]; - forEach(fn: any, ctx: any): this; - get(key: any, raw?: boolean): any[] | [any, any][]; - has(key: any): boolean; - indexKeys(arg?: string, delimiter?: string, data?: {}): any[]; - keys(): any[]; - limit(offset?: number, max?: number, raw?: boolean): any[] | [any, any][]; - list(...args: any[]): readonly any[]; - map(fn: any, raw?: boolean): any[] | [any, any][]; - merge(a: any, b: any, override?: boolean): any; - onbatch(arg: any, type?: string): any[]; - onclear(): void; - ondelete(key?: string, batch?: boolean): (string | boolean)[]; - onoverride(type?: string): any; - onset(arg?: {}, batch?: boolean): any[]; - override(data: any, type?: string): boolean; - reduce(fn: any, accumulator: any, raw?: boolean): any[] | [any, any][]; - reindex(index: any): this; - search(value: any, index: any, raw?: boolean): any[] | [any, any][]; - set(key?: any, data?: {}, batch?: boolean, override?: boolean): any; - setIndex(index: any, indexes: any, delimiter: any, key: any, data: any, indice: any): void; - sort(fn: any, frozen?: boolean): any[]; - sortBy(index?: string, raw?: boolean): any[] | [any, any][]; - toArray(frozen?: boolean): any[]; - uuid(): string; - values(): any; - where(predicate?: {}, raw?: boolean, op?: string): any[] | [any, any][]; + data: Map; + delimiter: string; + id: string; + immutable: boolean; + index: string[]; + indexes: Map>>; + key: string; + versions: Map>; + versioning: boolean; + readonly registry: string[]; + readonly size: number; + + /** + * Creates a new Haro instance with specified configuration + * @param config - Configuration object for the store + */ + constructor(config?: HaroConfig); + + /** + * Performs batch operations on multiple records for efficient bulk processing + * @param args - Array of records to process + * @param type - Type of operation: 'set' for upsert, 'del' for delete + * @returns Array of results from the batch operation + */ + batch(args: any[], type?: string): any[]; + + /** + * Lifecycle hook executed before batch operations for custom preprocessing + * @param arg - Arguments passed to batch operation + * @param type - Type of batch operation ('set' or 'del') + * @returns The arguments array (possibly modified) to be processed + */ + beforeBatch(arg: any, type?: string): any; + + /** + * Lifecycle hook executed before clear operation for custom preprocessing + */ + beforeClear(): void; + + /** + * Lifecycle hook executed before delete operation for custom preprocessing + * @param key - Key of record to delete + * @param batch - Whether this is part of a batch operation + */ + beforeDelete(key?: string, batch?: boolean): void; + + /** + * Lifecycle hook executed before set operation for custom preprocessing + * @param key - Key of record to set + * @param data - Record data being set + * @param batch - Whether this is part of a batch operation + * @param override - Whether to override existing data + */ + beforeSet(key?: string, data?: any, batch?: boolean, override?: boolean): void; + + /** + * Removes all records, indexes, and versions from the store + * @returns This instance for method chaining + */ + clear(): Haro; + + /** + * Creates a deep clone of the given value, handling objects, arrays, and primitives + * @param arg - Value to clone (any type) + * @returns Deep clone of the argument + */ + clone(arg: any): any; + + /** + * Deletes a record from the store and removes it from all indexes + * @param key - Key of record to delete + * @param batch - Whether this is part of a batch operation + * @throws Throws error if record with the specified key is not found + */ + delete(key?: string, batch?: boolean): void; + + /** + * Internal method to remove entries from indexes for a deleted record + * @param key - Key of record being deleted + * @param data - Data of record being deleted + * @returns This instance for method chaining + */ + deleteIndex(key: string, data: any): Haro; + + /** + * Exports complete store data or indexes for persistence or debugging + * @param type - Type of data to export: 'records' or 'indexes' + * @returns Array of [key, value] pairs for records, or serialized index structure + */ + dump(type?: string): any[]; + + /** + * Utility method to iterate over an array with a callback function + * @param arr - Array to iterate over + * @param fn - Function to call for each element (element, index) + * @returns The original array for method chaining + */ + each(arr: any[], fn: (value: any, index: number) => void): any[]; + + /** + * Returns an iterator of [key, value] pairs for each record in the store + * @returns Iterator of [key, value] pairs + */ + entries(): IterableIterator<[string, any]>; + + /** + * Finds records matching the specified criteria using indexes for optimal performance + * @param where - Object with field-value pairs to match against + * @param raw - Whether to return raw data without processing + * @returns Array of matching records (frozen if immutable mode) + */ + find(where?: Record, raw?: boolean): any[]; + + /** + * Filters records using a predicate function, similar to Array.filter + * @param fn - Predicate function to test each record (record, key, store) + * @param raw - Whether to return raw data without processing + * @returns Array of records that pass the predicate test + */ + filter(fn: (value: any) => boolean, raw?: boolean): any[]; + + /** + * Executes a function for each record in the store, similar to Array.forEach + * @param fn - Function to execute for each record (value, key) + * @param ctx - Context object to use as 'this' when executing the function + * @returns This instance for method chaining + */ + forEach(fn: (value: any, key: string) => void, ctx?: any): Haro; + + /** + * Creates a frozen array from the given arguments for immutable data handling + * @param args - Arguments to freeze into an array + * @returns Frozen array containing frozen arguments + */ + freeze(...args: any[]): readonly any[]; + + /** + * Retrieves a record by its key + * @param key - Key of record to retrieve + * @param raw - Whether to return raw data (true) or processed/frozen data (false) + * @returns The record if found, null if not found + */ + get(key: string, raw?: boolean): any | null; + + /** + * Checks if a record with the specified key exists in the store + * @param key - Key to check for existence + * @returns True if record exists, false otherwise + */ + has(key: string): boolean; + + /** + * Generates index keys for composite indexes from data values + * @param arg - Composite index field names joined by delimiter + * @param delimiter - Delimiter used in composite index + * @param data - Data object to extract field values from + * @returns Array of generated index keys + */ + indexKeys(arg?: string, delimiter?: string, data?: Record): string[]; + + /** + * Returns an iterator of all keys in the store + * @returns Iterator of record keys + */ + keys(): IterableIterator; + + /** + * Returns a limited subset of records with offset support for pagination + * @param offset - Number of records to skip from the beginning + * @param max - Maximum number of records to return + * @param raw - Whether to return raw data without processing + * @returns Array of records within the specified range + */ + limit(offset?: number, max?: number, raw?: boolean): any[]; + + /** + * Converts a record into a [key, value] pair array format + * @param arg - Record object to convert to list format + * @returns Array containing [key, record] where key is extracted from record's key field + */ + list(arg: any): any[]; + + /** + * Transforms all records using a mapping function, similar to Array.map + * @param fn - Function to transform each record (record, key) + * @param raw - Whether to return raw data without processing + * @returns Array of transformed results + */ + map(fn: (value: any, key: string) => any, raw?: boolean): any[]; + + /** + * Internal helper method for predicate matching with support for arrays and regex + * @param record - Record to test against predicate + * @param predicate - Predicate object with field-value pairs + * @param op - Operator for array matching ('||' for OR, '&&' for AND) + * @returns True if record matches predicate criteria + */ + matchesPredicate(record: any, predicate: Record, op: string): boolean; + + /** + * Merges two values together with support for arrays and objects + * @param a - First value (target) + * @param b - Second value (source) + * @param override - Whether to override arrays instead of concatenating + * @returns Merged result + */ + merge(a: any, b: any, override?: boolean): any; + + /** + * Lifecycle hook executed after batch operations for custom postprocessing + * @param arg - Result of batch operation + * @param type - Type of batch operation that was performed + * @returns Modified result (override this method to implement custom logic) + */ + onbatch(arg: any, type?: string): any; + + /** + * Lifecycle hook executed after clear operation for custom postprocessing + */ + onclear(): void; + + /** + * Lifecycle hook executed after delete operation for custom postprocessing + * @param key - Key of deleted record + * @param batch - Whether this was part of a batch operation + */ + ondelete(key?: string, batch?: boolean): void; + + /** + * Lifecycle hook executed after override operation for custom postprocessing + * @param type - Type of override operation that was performed + */ + onoverride(type?: string): void; + + /** + * Lifecycle hook executed after set operation for custom postprocessing + * @param arg - Record that was set + * @param batch - Whether this was part of a batch operation + */ + onset(arg?: any, batch?: boolean): void; + + /** + * Replaces all store data or indexes with new data for bulk operations + * @param data - Data to replace with (format depends on type) + * @param type - Type of data: 'records' or 'indexes' + * @returns True if operation succeeded + */ + override(data: any[], type?: string): boolean; + + /** + * Reduces all records to a single value using a reducer function + * @param fn - Reducer function (accumulator, value, key, store) + * @param accumulator - Initial accumulator value + * @returns Final reduced value + */ + reduce(fn: (accumulator: any, value: any, key: string, store: Haro) => any, accumulator?: any): any; + + /** + * Rebuilds indexes for specified fields or all fields for data consistency + * @param index - Specific index field(s) to rebuild, or all if not specified + * @returns This instance for method chaining + */ + reindex(index?: string | string[]): Haro; + + /** + * Searches for records containing a value across specified indexes + * @param value - Value to search for (string, function, or RegExp) + * @param index - Index(es) to search in, or all if not specified + * @param raw - Whether to return raw data without processing + * @returns Array of matching records + */ + search(value: any, index?: string | string[], raw?: boolean): any[]; + + /** + * Sets or updates a record in the store with automatic indexing + * @param key - Key for the record, or null to use record's key field + * @param data - Record data to set + * @param batch - Whether this is part of a batch operation + * @param override - Whether to override existing data instead of merging + * @returns The stored record (frozen if immutable mode) + */ + set(key?: string | null, data?: any, batch?: boolean, override?: boolean): any; + + /** + * Internal method to add entries to indexes for a record + * @param key - Key of record being indexed + * @param data - Data of record being indexed + * @param indice - Specific index to update, or null for all + * @returns This instance for method chaining + */ + setIndex(key: string, data: any, indice?: string | null): Haro; + + /** + * Sorts all records using a comparator function + * @param fn - Comparator function for sorting (a, b) => number + * @param frozen - Whether to return frozen records + * @returns Sorted array of records + */ + sort(fn: (a: any, b: any) => number, frozen?: boolean): any[]; + + /** + * Comparator function for sorting keys with type-aware comparison logic + * @param a - First value to compare + * @param b - Second value to compare + * @returns Negative number if a < b, positive if a > b, zero if equal + */ + sortKeys(a: any, b: any): number; + + /** + * Sorts records by a specific indexed field in ascending order + * @param index - Index field name to sort by + * @param raw - Whether to return raw data without processing + * @returns Array of records sorted by the specified field + */ + sortBy(index?: string, raw?: boolean): any[]; + + /** + * Converts all store data to a plain array of records + * @returns Array containing all records in the store + */ + toArray(): any[]; + + /** + * Generates a RFC4122 v4 UUID for record identification + * @returns UUID string in standard format + */ + uuid(): string; + + /** + * Returns an iterator of all values in the store + * @returns Iterator of record values + */ + values(): IterableIterator; + + /** + * Advanced filtering with predicate logic supporting AND/OR operations on arrays + * @param predicate - Object with field-value pairs for filtering + * @param op - Operator for array matching ('||' for OR, '&&' for AND) + * @returns Array of records matching the predicate criteria + */ + where(predicate?: Record, op?: string): any[]; } + +/** + * Factory function to create a new Haro instance with optional initial data + * @param data - Initial data to populate the store + * @param config - Configuration object passed to Haro constructor + * @returns New Haro instance configured and optionally populated + */ +export function haro(data?: any[] | null, config?: HaroConfig): Haro; \ No newline at end of file diff --git a/types/uuid.d.ts b/types/uuid.d.ts deleted file mode 100644 index c57035e1..00000000 --- a/types/uuid.d.ts +++ /dev/null @@ -1 +0,0 @@ -export const uuid: any;