UNPKG

11.9 kBJavaScriptView Raw
1/**
2 * Harō is a modern immutable DataStore
3 *
4 * @author Jason Mulligan <jason.mulligan@avoidwork.com>
5 * @copyright 2020
6 * @license BSD-3-Clause
7 * @version 8.0.2
8 */
9"use strict";
10
11(function (global) {
12 const r = [8, 9, "a", "b"];
13
14 function clone (arg) {
15 return JSON.parse(JSON.stringify(arg, null, 0));
16 }
17
18 function each (arr, fn) {
19 for (const item of arr.entries()) {
20 fn(item[1], item[0]);
21 }
22
23 return arr;
24 }
25
26 function keyIndex (key, data, delimiter, pattern) {
27 let result;
28
29 if (key.includes(delimiter)) {
30 result = key.split(delimiter).sort((a, b) => a.localeCompare(b)).map(i => (data[i] !== void 0 ? data[i] : "").toString().replace(new RegExp(pattern, "g"), "").toLowerCase()).join(delimiter);
31 } else {
32 result = data[key];
33 }
34
35 return result;
36 }
37
38 function delIndex (index, indexes, delimiter, key, data, pattern) {
39 index.forEach(i => {
40 const idx = indexes.get(i),
41 value = keyIndex(i, data, delimiter, pattern);
42
43 if (idx.has(value)) {
44 const o = idx.get(value);
45
46 o.delete(key);
47
48 if (o.size === 0) {
49 idx.delete(value);
50 }
51 }
52 });
53 }
54
55 function merge (a, b) {
56 if (a instanceof Object && b instanceof Object) {
57 each(Object.keys(b), i => {
58 if (a[i] instanceof Object && b[i] instanceof Object) {
59 a[i] = merge(a[i], b[i]);
60 } else if (Array.isArray(a[i]) && Array.isArray(b[i])) {
61 a[i] = a[i].concat(b[i]);
62 } else {
63 a[i] = b[i];
64 }
65 });
66 } else if (Array.isArray(a) && Array.isArray(b)) {
67 a = a.concat(b);
68 } else {
69 a = b;
70 }
71
72 return a;
73 }
74
75 function s () {
76 return ((Math.random() + 1) * 0x10000 | 0).toString(16).substring(1);
77 }
78
79 function setIndex (index, indexes, delimiter, key, data, indice, pattern) {
80 each(!indice ? index : [indice], i => {
81 const lindex = indexes.get(i);
82
83 if (Array.isArray(data[i]) && !i.includes(delimiter)) {
84 each(data[i], d => {
85 if (!lindex.has(d)) {
86 lindex.set(d, new Set());
87 }
88
89 lindex.get(d).add(key);
90 });
91 } else {
92 const lidx = keyIndex(i, data, delimiter, pattern);
93
94 if (lidx !== void 0 && lidx !== null) {
95 if (!lindex.has(lidx)) {
96 lindex.set(lidx, new Set());
97 }
98
99 lindex.get(lidx).add(key);
100 }
101 }
102 });
103 }
104
105 function uuid () {
106 return s() + s() + "-" + s() + "-4" + s().substr(0, 3) + "-" + r[Math.floor(Math.random() * 4)] + s().substr(0, 3) + "-" + s() + s() + s();
107 }
108
109 class Haro {
110 constructor ({delimiter = "|", id = uuid(), index = [], key = "", pattern = "\\s*|\\t*", versioning = false} = {}) {
111 this.data = new Map();
112 this.delimiter = delimiter;
113 this.id = id;
114 this.index = index;
115 this.indexes = new Map();
116 this.key = key;
117 this.pattern = pattern;
118 this.size = 0;
119 this.versions = new Map();
120 this.versioning = versioning;
121
122 Object.defineProperty(this, "registry", {
123 enumerable: true,
124 get: () => Array.from(this.data.keys())
125 });
126
127 return this.reindex();
128 }
129
130 async batch (args, type = "set", lazyLoad = false) {
131 let result;
132
133 try {
134 const fn = type === "del" ? i => this.del(i, true, lazyLoad) : i => this.set(null, i, true, true, lazyLoad);
135
136 result = await Promise.all(this.beforeBatch(args, type).map(fn));
137 result = this.onbatch(result, type);
138 } catch (e) {
139 this.onerror("batch", e);
140 throw e;
141 }
142
143 return result;
144 }
145
146 beforeBatch (arg) {
147 return arg;
148 }
149
150 beforeClear () {}
151
152 beforeDelete () {}
153
154 beforeSet () {}
155
156 clear () {
157 this.beforeClear();
158 this.size = 0;
159 this.data.clear();
160 this.indexes.clear();
161 this.versions.clear();
162 this.reindex().onclear();
163
164 return this;
165 }
166
167 del (key, batch = false, lazyLoad = false, retry = false) {
168 if (this.has(key) === false) {
169 throw new Error("Record not found");
170 }
171
172 const og = this.get(key, true);
173
174 return this.exec(async () => {
175 this.beforeDelete(key, batch, lazyLoad, retry);
176 delIndex(this.index, this.indexes, this.delimiter, key, og, this.pattern);
177 this.data.delete(key);
178 --this.size;
179 }, async () => {
180 this.ondelete(key, batch, retry, lazyLoad);
181
182 if (this.versioning) {
183 this.versions.delete(key);
184 }
185 }, err => {
186 this.onerror("delete", err);
187 throw err;
188 });
189 }
190
191 dump (type = "records") {
192 const result = type === "records" ? Array.from(this.entries()) : Object.fromEntries(this.indexes);
193
194 if (type === "indexes") {
195 for (const key of Object.keys(result)) {
196 result[key] = Object.fromEntries(result[key]);
197
198 for (const lkey of Object.keys(result[key])) {
199 result[key][lkey] = Array.from(result[key][lkey]);
200 }
201 }
202 }
203
204 return result;
205 }
206
207 entries () {
208 return this.data.entries();
209 }
210
211 async exec (first, second, handler) {
212 let result;
213
214 try {
215 result = await second(await first());
216 } catch (err) {
217 handler(err);
218 }
219
220 return result;
221 }
222
223 find (where, raw = false) {
224 const key = Object.keys(where).sort((a, b) => a.localeCompare(b)).join(this.delimiter),
225 value = keyIndex(key, where, this.delimiter, this.pattern),
226 result = Array.from((this.indexes.get(key) || new Map()).get(value) || new Set()).map(i => this.get(i, raw));
227
228 return raw ? result : this.list(...result);
229 }
230
231 filter (fn, raw = false) {
232 const x = raw ? g => g : g => this.get(g, raw),
233 result = this.reduce((a, v, k, ctx) => {
234 if (fn.call(ctx, v)) {
235 a.push(x(k));
236 }
237
238 return a;
239 }, []);
240
241 return raw ? result : this.list(...result);
242 }
243
244 forEach (fn, ctx) {
245 this.data.forEach((value, key) => fn(clone(value), clone(key)), ctx || this.data);
246
247 return this;
248 }
249
250 get (key, raw = false) {
251 const result = clone(this.data.get(key) || null);
252
253 return raw ? result : this.list(key, result);
254 }
255
256 has (key, map = this.data) {
257 return map.has(key);
258 }
259
260 keys () {
261 return this.data.keys();
262 }
263
264 limit (offset = 0, max = 0, raw = false) {
265 const result = this.registry.slice(offset, offset + max).map(i => this.get(i, raw));
266
267 return raw ? result : this.list(...result);
268 }
269
270 list (...args) {
271 return Object.freeze(args.map(i => Object.freeze(i)));
272 }
273
274 map (fn, raw = false) {
275 const result = [];
276
277 this.forEach((value, key) => result.push(fn(value, key)));
278
279 return raw ? result : this.list(...result);
280 }
281
282 onbatch (arg) {
283 return arg;
284 }
285
286 onclear () {}
287
288 ondelete () {}
289
290 onerror () {}
291
292 onset () {}
293
294 async override (data, type = "records") {
295 const result = true;
296
297 if (type === "indexes") {
298 this.indexes = new Map(Object.keys(data).map(i => [i, new Map(Object.keys(data[i]).map(p => [p, new Set(data[i][p])]))]));
299 } else if (type === "records") {
300 this.indexes.clear();
301 this.data = new Map(data);
302 this.size = this.data.size;
303 } else {
304 throw new Error("Invalid type");
305 }
306
307 return result;
308 }
309
310 reduce (fn, accumulator, raw = false) {
311 let a = accumulator || this.data.keys().next().value;
312
313 this.forEach((v, k) => {
314 a = fn(a, v, k, this, raw);
315 }, this);
316
317 return a;
318 }
319
320 reindex (index) {
321 const indices = index ? [index] : this.index;
322
323 if (index && this.index.includes(index) === false) {
324 this.index.push(index);
325 }
326
327 each(indices, i => this.indexes.set(i, new Map()));
328 this.forEach((data, key) => each(indices, i => setIndex(this.index, this.indexes, this.delimiter, key, data, i, this.pattern)));
329
330 return this;
331 }
332
333 search (value, index, raw = false) {
334 const result = new Map(),
335 fn = typeof value === "function",
336 rgex = value && typeof value.test === "function";
337
338 if (value) {
339 each(index ? Array.isArray(index) ? index : [index] : this.index, i => {
340 let idx = this.indexes.get(i);
341
342 if (idx) {
343 idx.forEach((lset, lkey) => {
344 switch (true) {
345 case fn && value(lkey, i):
346 case rgex && value.test(Array.isArray(lkey) ? lkey.join(", ") : lkey):
347 case lkey === value:
348 lset.forEach(key => {
349 if (!result.has(key) && this.has(key)) {
350 result.set(key, this.get(key, raw));
351 }
352 });
353 break;
354 default:
355 void 0;
356 }
357 });
358 }
359 });
360 }
361
362 return raw ? Array.from(result.values()) : this.list(...Array.from(result.values()));
363 }
364
365 async set (key, data, batch = false, override = false, lazyLoad = false, retry = false) {
366 let x = clone(data),
367 og;
368
369 return this.exec(async () => {
370 if (key === void 0 || key === null) {
371 key = this.key && x[this.key] !== void 0 ? x[this.key] : uuid();
372 }
373
374 this.beforeSet(key, data, batch, override, lazyLoad, retry);
375
376 if (!this.data.has(key)) {
377 ++this.size;
378
379 if (this.versioning) {
380 this.versions.set(key, new Set());
381 }
382 } else {
383 og = this.get(key, true);
384 delIndex(this.index, this.indexes, this.delimiter, key, og, this.pattern);
385
386 if (this.versioning) {
387 this.versions.get(key).add(Object.freeze(clone(og)));
388 }
389
390 if (override === false) {
391 x = merge(clone(og), x);
392 }
393 }
394
395 this.data.set(key, x);
396 setIndex(this.index, this.indexes, this.delimiter, key, x, null, this.pattern);
397
398 return this.get(key);
399 }, async arg => {
400 this.onset(arg, batch, retry, lazyLoad);
401
402 return arg;
403 }, err => {
404 this.onerror("set", err);
405 throw err;
406 });
407 }
408
409 sort (fn, frozen = true) {
410 return frozen ? Object.freeze(this.limit(0, this.size, true).sort(fn).map(i => Object.freeze(i))) : this.limit(0, this.size, true).sort(fn);
411 }
412
413 sortBy (index, raw = false) {
414 const result = [],
415 keys = [];
416
417 let lindex;
418
419 if (!this.indexes.has(index)) {
420 this.reindex(index);
421 }
422
423 lindex = this.indexes.get(index);
424 lindex.forEach((idx, key) => keys.push(key));
425 each(keys.sort(), i => lindex.get(i).forEach(key => result.push(this.get(key, raw))));
426
427 return raw ? result : this.list(...result);
428 }
429
430 toArray (frozen = true) {
431 const result = Array.from(this.data.values()).map(i => clone(i));
432
433 if (frozen) {
434 each(result, i => Object.freeze(i));
435 Object.freeze(result);
436 }
437
438 return result;
439 }
440
441 values () {
442 return this.data.values();
443 }
444
445 where (predicate, raw = false, op = "||") {
446 const keys = this.index.filter(i => i in predicate);
447
448 return keys.length > 0 ? this.filter(new Function("a", `return (${keys.map(i => {
449 let result;
450
451 if (Array.isArray(predicate[i])) {
452 result = `Array.isArray(a['${i}']) ? ${predicate[i].map(arg => `a['${i}'].includes(${typeof arg === "string" ? `'${arg}'` : arg})`).join(` ${op} `)} : a['${i}'] === '${predicate[i].join(",")}'`;
453 } else if (predicate[i] instanceof RegExp) {
454 result = `Array.isArray(a['${i}']) ? a['${i}'].filter(i => ${predicate[i]}.test(a['${i}'])).length > 0 : ${predicate[i]}.test(a['${i}'])`;
455 } else {
456 const arg = typeof predicate[i] === "string" ? `'${predicate[i]}'` : predicate[i];
457
458 result = `Array.isArray(a['${i}']) ? a['${i}'].includes(${arg}) : a['${i}'] === ${arg}`;
459 }
460
461 return result;
462 }).join(") && (")});`), raw) : [];
463 }
464 }
465
466 function factory (data = null, config = {}) {
467 const obj = new Haro(config);
468
469 if (Array.isArray(data)) {
470 obj.batch(data, "set");
471 }
472
473 return obj;
474 }
475
476 factory.version = "8.0.2";
477
478 // Node, AMD & window supported
479 if (typeof exports !== "undefined") {
480 module.exports = factory;
481 } else if (typeof define === "function" && define.amd !== void 0) {
482 define(() => factory);
483 } else {
484 global.haro = factory;
485 }
486}(typeof window !== "undefined" ? window : global));