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.
- 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.
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.”
- 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.
Install with npm:
npm install haro
Or with yarn:
yarn add haro
Haro is available as both an ES module and CommonJS module.
import { haro } from 'haro';
const { haro } = require('haro');
Haro takes two optional arguments: an array of records to set asynchronously, and a configuration object.
const storeDefaults = haro();
const storeRecords = haro([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 28 }
]);
const storeCustom = haro(null, { key: 'id' });
import { haro } from 'haro';
// Create a store with indexes for name and email
const contacts = haro(null, { index: ['name', 'email'] });
// Add realistic contacts
contacts.batch([
{ name: 'Alice Johnson', email: '[email protected]', company: 'Acme Corp', phone: '555-1234' },
{ name: 'Carlos Rivera', email: '[email protected]', company: 'Rivera Designs', phone: '555-5678' },
{ name: 'Priya Patel', email: '[email protected]', company: 'InnovateX', phone: '555-8765' }
], 'set');
// Find a contact by email
console.log(contacts.find({ email: '[email protected]' }));
// → [[$uuid, { name: 'Carlos Rivera', email: 'carlos.r@example.com', company: 'Rivera Designs', phone: '555-5678' }]]
// 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' }]]
// Search contacts with phone numbers ending in '78'
console.log(contacts.search(phone => phone.endsWith('78'), 'phone'));
// → [[$uuid, { name: 'Carlos Rivera', ... }]]
import { haro } from 'haro';
// Create a store for project tasks, indexed by status and assignee
const tasks = haro(null, { index: ['status', 'assignee'] });
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');
// Find all open tasks
console.log(tasks.find({ status: 'open' }));
// → [[$uuid, { title: 'Fix login bug', status: 'open', assignee: 'Carlos', due: '2025-05-18' }]]
// Search tasks assigned to Alice
console.log(tasks.search('Alice', 'assignee'));
// → [[$uuid, { title: 'Design homepage', ... }]]
import { haro } from 'haro';
// Enable versioning for order tracking
const orders = haro(null, { versioning: true });
// 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' });
// 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' }
// { note: 'Initial' }
// { note: 'Updated' }
These examples show how Haro can help you manage contacts, tasks, and keep a history of changes with just a few lines of code.
Function
Event listener for before a batch operation, receives type
, data
.
Function
Event listener for before clearing the data store.
Function
Event listener for before a record is deleted, receives key
, batch
.
Function
Event listener for before a record is set, receives key
, data
.
Array
Array of values to index. Composite indexes are supported, by using the default delimiter (this.delimiter
).
Non-matches within composites result in blank values.
Example of fields/properties to index:
const store = haro(null, {index: ['field1', 'field2', 'field1|field2|field3']});
String
Optional Object
key to utilize as Map
key, defaults to a version 4 UUID
if not specified, or found.
Example of specifying the primary key:
const store = haro(null, {key: 'field'});
Boolean
Logs persistent storage messages to console
, default is true
.
Function
Event listener for a batch operation, receives two arguments ['type', Array
].
Function
Event listener for clearing the data store.
Function
Event listener for when a record is deleted, receives the record key.
Function
Event listener for when the data store changes entire data set, receives a String
naming what changed (indexes
or records
).
Function
Event listener for when a record is set, receives an Array
.
Boolean
Enable/disable MVCC style versioning of records, default is false
. Versions are stored in Sets
for easy iteration.
Example of enabling versioning:
const store = haro(null, {versioning: true});
Map
Map
of records, updated by del()
& set()
.
Map
Map of indexes, which are Sets containing Map keys.
Array
Array representing the order of this.data
.
Number
Number of records in the DataStore.
Map
Map
of Sets
of records, updated by set()
.
Array
The first argument must be an Array
, and the second argument must be del
or set
.
const haro = require('haro'),
store = haro(null, {key: 'id', index: ['name']}),
nth = 100,
data = [];
let i = -1;
while (++i < nth) {
data.push({id: i, name: 'John Doe' + i});
}
// records is an Array of Arrays
const records = store.batch(data, 'set');
self
Removes all key/value pairs from the DataStore.
Example of clearing a DataStore:
const store = haro();
// Data is added
store.clear();
Undefined
Deletes the record.
Example of deleting a record:
const store = haro(),
rec = store.set(null, {abc: true});
store.del(rec[0]);
console.log(store.size); // 0
Array or Object
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.
const store = haro();
// Data is loaded
const records = store.dump();
const indexes = store.dump('indexes');
// Save records & indexes
MapIterator
Returns a new Iterator
object that contains an array of [key, value]
for each element in the Map
object in
insertion order.
Example of deleting a record:
const store = haro();
let item, iterator;
// Data is added
iterator = store.entries();
item = iterator.next();
do {
console.log(item.value);
item = iterator.next();
} while (!item.done);
Array
Returns an Array
of double Arrays
with the shape [key, value]
for records which returned true
to
callbackFn(value, key)
.
Example of filtering a DataStore:
const store = haro();
// Data is added
store.filter(function (value) {
return value.something === true;
});
Array
Returns an Array
of double Arrays
with found by indexed values matching the where
.
Example of finding a record(s) with an identity match:
const store = haro(null, {index: ['field1']});
// Data is added
store.find({field1: 'some value'});
Undefined
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.
Example of deleting a record:
const store = haro();
store.set(null, {abc: true});
store.forEach(function (value, key) {
console.log(key);
});
Array
Gets the record as a double Array
with the shape [key, value]
.
Example of getting a record with a known primary key value:
const store = haro();
// Data is added
store.get('keyValue');
Boolean
Returns a Boolean
indicating if the data store contains key
.
Example of checking for a record with a known primary key value:
const store = haro();
// Data is added
store.has('keyValue'); // true or false
MapIterator
Returns a new Iterator
object that contains the keys for each element in the Map
object in insertion order.`
Example of getting an iterator, and logging the results:
const store = haro();
let item, iterator;
// Data is added
iterator = store.keys();
item = iterator.next();
do {
console.log(item.value);
item = iterator.next();
} while (!item.done);
Array
Returns an Array
of double Arrays
with the shape [key, value]
for the corresponding range of records.
Example of paginating a data set:
const store = haro();
let ds1, ds2;
// Data is added
console.log(store.size); // >10
ds1 = store.limit(0, 10); // [0-9]
ds2 = store.limit(10, 10); // [10-19]
console.log(ds1.length === ds2.length); // true
console.log(JSON.stringify(ds1[0][1]) === JSON.stringify(ds2[0][1])); // false
Array
Returns an Array
of the returns of callbackFn(value, key)
. If raw
is true
an Array
is returned.
Example of mapping a DataStore:
const store = haro();
// Data is added
store.map(function (value) {
return value.property;
});
Boolean
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.
Example of overriding a DataStore:
const store = haro();
store.override({'field': {'value': ['pk']}}, "indexes");
Array
Runs an Array.reduce()
inspired function against the data store (Map
).
Example of filtering a DataStore:
const store = haro();
// Data is added
store.reduce(function (accumulator, value, key) {
accumulator[key] = value;
return accumulator;
}, {});
Haro
Re-indexes the DataStore, to be called if changing the value of index
.
Example of mapping a DataStore:
const store = haro();
// Data is added
// Creating a late index
store.reindex('field3');
// Recreating indexes, this should only happen if the store is out of sync caused by developer code.
store.reindex();
Array
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
.
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
.
Example of searching with a predicate function:
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 }
];
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 }]]
Object
Record in the DataStore. If key
is false
a version 4 UUID
will be
generated.
If override
is true
, the existing record will be replaced instead of amended.
Example of creating a record:
const store = haro(null, {key: 'id'}),
record = store.set(null, {id: 1, name: 'John Doe'});
console.log(record); // [1, {id: 1, name: 'Jane Doe'}]
Array
Returns an Array of the DataStore, sorted by callbackFn
.
Example of sorting like an Array
:
const store = haro(null, {index: ['name', 'age']}),
data = [{name: 'John Doe', age: 30}, {name: 'Jane Doe', age: 28}];
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}]
Array
Returns an Array
of double Arrays
with the shape [key, value]
of records sorted by an index.
Example of sorting by an index:
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' }
];
store.batch(tickets, 'set');
console.log(store.sortBy('priority'));
// → Sorted by priority ascending
Array
Returns an Array of the DataStore.
Example of casting to an Array
:
const store = haro(),
notes = [
{ title: 'Call Alice', content: 'Discuss Q2 roadmap.' },
{ title: 'Email Carlos', content: 'Send project update.' }
];
store.batch(notes, 'set');
console.log(store.toArray());
// → [
// { title: 'Call Alice', content: 'Discuss Q2 roadmap.' },
// { title: 'Email Carlos', content: 'Send project update.' }
// ]
MapIterator
Returns a new Iterator
object that contains the values for each element in the Map
object in insertion order.
Example of iterating the values:
const store = haro(),
data = [{name: 'John Doe', age: 30}, {name: 'Jane Doe', age: 28}];
store.batch(data, 'set')
const iterator = store.values();
let item = iterator.next();
while (!item.done) {
console.log(item.value);
item = iterator.next();
};
Array
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()
.
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}];
store.batch(data, 'set');
console.log(store.where({name: 'John Doe', age: 30})); // [{guid: 'abc', name: 'John Doe', age: 30}]
Contributions, issues, and feature requests are welcome! Feel free to check the issues page or submit a pull request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/my-feature
) - Commit your changes (
git commit -am 'Add new feature'
) - Push to the branch (
git push origin feature/my-feature
) - Open a pull request
For questions, suggestions, or support, please open an issue on GitHub, or contact the maintainer.
This project is licensed under the BSD-3 license - see the LICENSE file for details.
See CHANGELOG.md for release notes and version history.