1
0
mirror of https://github.com/nextapps-de/flexsearch.git synced 2025-09-25 21:08:59 +02:00
Files
flexsearch/dist/module-debug/db/indexeddb/index.js
2025-04-14 14:22:10 +02:00

671 lines
21 KiB
JavaScript

import { PersistentOptions, SearchResults, EnrichedSearchResults } from "../../type.js";
const VERSION = 1,
IndexedDB = "undefined" != typeof window && (window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB),
IDBTransaction = "undefined" != typeof window && (window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction),
IDBKeyRange = "undefined" != typeof window && (window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange),
fields = ["map", "ctx", "tag", "reg", "cfg"];
import StorageInterface from "../interface.js";
import { create_object, toArray } from "../../common.js";
/**
* @param {!string} str
* @return {string}
*/
function sanitize(str) {
return str.toLowerCase().replace(/[^a-z0-9_\-]/g, "");
}
const Index = create_object();
/**
* @param {string|PersistentOptions=} name
* @param {PersistentOptions=} config
* @constructor
* @implements StorageInterface
*/
export default function IdxDB(name, config = {}) {
if (!this || this.constructor !== IdxDB) {
return new IdxDB(name, config);
}
if ("object" == typeof name) {
config = /** @type PersistentOptions */name;
name = name.name;
}
if (!name) {
console.info("Default storage space was used, because a name was not passed.");
}
this.id = "flexsearch" + (name ? ":" + sanitize(name) : "");
this.field = config.field ? sanitize(config.field) : "";
this.type = config.type;
this.support_tag_search = !1;
this.fastupdate = !1;
this.db = null;
this.trx = {};
}
IdxDB.prototype.mount = function (flexsearch) {
//if(flexsearch.constructor === Document){
if (flexsearch.index) {
return flexsearch.mount(this);
}
flexsearch.db = this;
return this.open();
};
IdxDB.prototype.open = function () {
if (this.db) return this.db;
let self = this;
navigator.storage && navigator.storage.persist();
// return this.db = new Promise(function(resolve, reject){
Index[self.id] || (Index[self.id] = []);
Index[self.id].push(self.field);
const req = IndexedDB.open(self.id, VERSION);
/** @this {IDBOpenDBRequest} */
req.onupgradeneeded = function () {
const db = self.db = this.result;
// Using Indexes + IDBKeyRange on schema map => [key, res, id] performs
// too bad and blows up amazingly in size
// The schema map:key => [res][id] is currently used instead
// In fact that bypass the idea of a storage solution,
// IndexedDB is such a poor contribution :(
for (let i = 0, ref; i < fields.length; i++) {
ref = fields[i];
for (let j = 0, field; j < Index[self.id].length; j++) {
field = Index[self.id][j];
db.objectStoreNames.contains(ref + ("reg" !== ref ? field ? ":" + field : "" : "")) || db.createObjectStore(ref + ("reg" !== ref ? field ? ":" + field : "" : "")); //{ autoIncrement: true /*keyPath: "id"*/ }
//.createIndex("idx", "ids", { multiEntry: true, unique: false });
}
}
// switch(event.oldVersion){ // existing db version
// case 0:
// // version 0 means that the client had no database
// // perform initialization
// case 1:
// // client had version 1
// // update
// }
};
return self.db = promisfy(req, function (result) {
self.db = result; //event.target.result;
self.db.onversionchange = function () {
//database is outdated
self.close();
};
});
// req.onblocked = function(event) {
// // this event shouldn't trigger if we handle onversionchange correctly
// // it means that there's another open connection to the same database
// // and it wasn't closed after db.onversionchange triggered for it
// console.error("blocked", event);
// reject();
// };
//
// req.onerror = function(event){
// console.error(this.error, event);
// reject();
// };
//
// req.onsuccess = function(event){
// self.db = this.result; //event.target.result;
// self.db.onversionchange = function(){
// //database is outdated
// self.close();
// };
// resolve(self);
// };
// });
};
IdxDB.prototype.close = function () {
this.db && this.db.close();
this.db = null;
};
/**
* @return {!Promise<undefined>}
*/
IdxDB.prototype.destroy = function () {
const req = IndexedDB.deleteDatabase(this.id);
return promisfy(req);
};
// IdxDB.prototype.set = function(ref, key, ctx, data){
// const transaction = this.db.transaction(ref, "readwrite");
// const map = transaction.objectStore(ref);
// const req = map.put(data, ctx ? ctx + ":" + key : key);
// return transaction;//promisfy(req, callback);
// };
// IdxDB.prototype.delete = function(ref, key, ctx){
// const transaction = this.db.transaction(ref, "readwrite");
// const map = transaction.objectStore(ref);
// const req = map.delete(ctx ? ctx + ":" + key : key);
// return transaction;//promisfy(req, callback);
// };
/**
* @return {!Promise<undefined>}
*/
IdxDB.prototype.clear = function () {
const stores = [];
for (let i = 0, ref; i < fields.length; i++) {
ref = fields[i];
for (let j = 0, field; j < Index[this.id].length; j++) {
field = Index[this.id][j];
stores.push(ref + ("reg" !== ref ? field ? ":" + field : "" : ""));
}
}
const transaction = this.db.transaction(stores, "readwrite");
for (let i = 0; i < stores.length; i++) {
transaction.objectStore(stores[i]).clear();
}
return promisfy(transaction);
};
/**
* @param {!string} key
* @param {string=} ctx
* @param {number=} limit
* @param {number=} offset
* @param {boolean=} resolve
* @param {boolean=} enrich
* @return {!Promise<SearchResults|EnrichedSearchResults>}
*/
IdxDB.prototype.get = function (key, ctx, limit = 0, offset = 0, resolve = /* tag? */!0, enrich = !1) {
const transaction = this.db.transaction((ctx ? "ctx" : "map") + (this.field ? ":" + this.field : ""), "readonly"),
map = transaction.objectStore((ctx ? "ctx" : "map") + (this.field ? ":" + this.field : "")),
req = map.get(ctx ? ctx + ":" + key : key),
self = this;
return promisfy(req).then(function (res) {
let result = [];
if (!res || !res.length) return result;
if (resolve) {
if (!limit && !offset && 1 === res.length) {
return res[0];
}
for (let i = 0, arr; i < res.length; i++) {
if ((arr = res[i]) && arr.length) {
if (offset >= arr.length) {
offset -= arr.length;
continue;
}
const end = limit ? offset + Math.min(arr.length - offset, limit) : arr.length;
for (let j = offset; j < end; j++) {
result.push(arr[j]);
}
offset = 0;
if (result.length === limit) {
break;
}
}
}
return enrich ? self.enrich(result) : result;
} else {
return res;
}
});
};
/**
* @param {!string} tag
* @param {number=} limit
* @param {number=} offset
* @param {boolean=} enrich
* @return {!Promise<SearchResults|EnrichedSearchResults>}
*/
IdxDB.prototype.tag = function (tag, limit = 0, offset = 0, enrich = !1) {
const transaction = this.db.transaction("tag" + (this.field ? ":" + this.field : ""), "readonly"),
map = transaction.objectStore("tag" + (this.field ? ":" + this.field : "")),
req = map.get(tag),
self = this;
return promisfy(req).then(function (ids) {
if (!ids || !ids.length || offset >= ids.length) return [];
if (!limit && !offset) return ids;
const result = ids.slice(offset, offset + limit);
return enrich ? self.enrich(result) : result;
});
};
/**
* @param {SearchResults} ids
* @return {!Promise<EnrichedSearchResults>}
*/
IdxDB.prototype.enrich = function (ids) {
if ("object" != typeof ids) {
ids = [ids];
}
const transaction = this.db.transaction("reg", "readonly"),
map = transaction.objectStore("reg"),
promises = [];
for (let i = 0; i < ids.length; i++) {
promises[i] = promisfy(map.get(ids[i]));
}
return Promise.all(promises).then(function (docs) {
for (let i = 0; i < docs.length; i++) {
docs[i] = {
id: ids[i],
doc: docs[i] ? JSON.parse(docs[i]) : null
};
}
return docs;
});
};
/**
* @param {number|string} id
* @return {!Promise<undefined>}
*/
IdxDB.prototype.has = function (id) {
const transaction = this.db.transaction("reg", "readonly"),
map = transaction.objectStore("reg"),
req = map.getKey(id);
return promisfy(req).then(function (result) {
return !!result;
});
};
IdxDB.prototype.search = null;
// IdxDB.prototype.has = function(ref, key, ctx){
// const transaction = this.db.transaction(ref, "readonly");
// const map = transaction.objectStore(ref);
// const req = map.getKey(ctx ? ctx + ":" + key : key);
// return promisfy(req);
// };
IdxDB.prototype.info = function () {
// todo
};
/**
* @param {!string} ref
* @param {!string} modifier
* @param {!Function} task
*/
IdxDB.prototype.transaction = function (ref, modifier, task) {
const key = ref + ("reg" !== ref ? this.field ? ":" + this.field : "" : "");
/**
* @type {IDBObjectStore}
*/
let store = this.trx[key + ":" + modifier];
if (store) return task.call(this, store);
let transaction = this.db.transaction(key, modifier);
/**
* @type {IDBObjectStore}
*/
this.trx[key + ":" + modifier] = store = transaction.objectStore(key);
const promise = task.call(this, store);
this.trx[key + ":" + modifier] = null;
return promisfy(transaction).finally(function () {
transaction = store = null;
return promise;
});
// return new Promise((resolve, reject) => {
// transaction.onerror = (err) => {
// transaction.abort();
// transaction = store = null;
// reject(err);
// //db.close;
// };
// transaction.oncomplete = (res) => {
// transaction = store = null;
// resolve(res || true);
// //db.close;
// };
// const promise = task.call(this, store);
// // transactions can just be used within the same event loop
// // the indexeddb is such a stupid tool :(
// this.trx[key + ":" + modifier] = null;
// return promise;
// });
};
IdxDB.prototype.commit = async function (flexsearch, _replace, _append) {
// process cleanup tasks
if (_replace) {
await this.clear();
// there are just removals in the task queue
flexsearch.commit_task = [];
} else {
let tasks = flexsearch.commit_task;
flexsearch.commit_task = [];
for (let i = 0, task; i < tasks.length; i++) {
/** @dict */
task = tasks[i];
// there are just removals in the task queue
if (task.clear) {
await this.clear();
_replace = !0;
break;
} else {
tasks[i] = task.del;
}
}
if (!_replace) {
if (!_append) {
tasks = tasks.concat(toArray(flexsearch.reg));
}
tasks.length && (await this.remove(tasks));
}
}
if (!flexsearch.reg.size) {
return;
}
await this.transaction("map", "readwrite", function (store) {
for (const item of flexsearch.map) {
const key = item[0],
value = item[1];
if (!value.length) continue;
if (_replace) {
store.put(value, key);
continue;
}
store.get(key).onsuccess = function () {
let result = this.result,
changed;
if (result && result.length) {
const maxlen = Math.max(result.length, value.length);
for (let i = 0, res, val; i < maxlen; i++) {
val = value[i];
if (val && val.length) {
res = result[i];
if (res && res.length) {
for (let j = 0; j < val.length; j++) {
res.push(val[j]);
}
changed = 1;
//result[i] = res.concat(val);
//result[i] = new Set([...result[i], ...value[i]]);
//result[i] = result[i].union(new Set(value[i]));
} else {
result[i] = val;
changed = 1;
//result[i] = new Set(value[i])
}
}
}
} else {
result = value;
changed = 1;
//result = [];
//for(let i = 0; i < value.length; i++){
// if(value[i]) result[i] = new Set(value[i]);
//}
}
changed && store.put(result, key);
};
}
});
await this.transaction("ctx", "readwrite", function (store) {
for (const ctx of flexsearch.ctx) {
const ctx_key = ctx[0],
ctx_value = ctx[1];
for (const item of ctx_value) {
const key = item[0],
value = item[1];
if (!value.length) continue;
if (_replace) {
store.put(value, ctx_key + ":" + key);
continue;
}
store.get(ctx_key + ":" + key).onsuccess = function () {
let result = this.result,
changed;
if (result && result.length) {
const maxlen = Math.max(result.length, value.length);
for (let i = 0, res, val; i < maxlen; i++) {
val = value[i];
if (val && val.length) {
res = result[i];
if (res && res.length) {
for (let j = 0; j < val.length; j++) {
res.push(val[j]);
}
//result[i] = res.concat(val);
changed = 1;
} else {
result[i] = val;
changed = 1;
}
}
}
} else {
result = value;
changed = 1;
}
changed && store.put(result, ctx_key + ":" + key);
};
}
}
});
if (flexsearch.store) {
await this.transaction("reg", "readwrite", function (store) {
for (const item of flexsearch.store) {
const id = item[0],
doc = item[1];
store.put("object" == typeof doc ? JSON.stringify(doc) : 1, id);
}
});
} else if (!flexsearch.bypass) {
await this.transaction("reg", "readwrite", function (store) {
for (const id of flexsearch.reg.keys()) {
store.put(1, id);
}
});
}
if (flexsearch.tag) {
await this.transaction("tag", "readwrite", function (store) {
for (const item of flexsearch.tag) {
const tag = item[0],
ids = item[1];
if (!ids.length) continue;
store.get(tag).onsuccess = function () {
let result = this.result;
result = result && result.length ? result.concat(ids) : ids;
store.put(result, tag);
};
}
});
}
// TODO
// await this.transaction("cfg", "readwrite", function(store){
// store.put({
// "charset": flexsearch.charset,
// "tokenize": flexsearch.tokenize,
// "resolution": flexsearch.resolution,
// "fastupdate": flexsearch.fastupdate,
// "compress": flexsearch.compress,
// "encoder": {
// "minlength": flexsearch.encoder.minlength
// },
// "context": {
// "depth": flexsearch.depth,
// "bidirectional": flexsearch.bidirectional,
// "resolution": flexsearch.resolution_ctx
// }
// }, "current");
// });
flexsearch.map.clear();
flexsearch.ctx.clear();
flexsearch.tag && flexsearch.tag.clear();
flexsearch.store && flexsearch.store.clear();
flexsearch.document || flexsearch.reg.clear();
};
/**
* @param {IDBCursorWithValue} cursor
* @param {Array<number|string>} ids
* @param {boolean=} _tag
*/
function handle(cursor, ids, _tag) {
const arr = cursor.value;
let changed,
count = 0;
for (let x = 0, result; x < arr.length; x++) {
// tags has no resolution layer
if (result = _tag ? arr : arr[x]) {
for (let i = 0, pos, id; i < ids.length; i++) {
id = ids[i];
pos = result.indexOf(id);
if (0 <= pos) {
changed = 1;
if (1 < result.length) {
result.splice(pos, 1);
} else {
arr[x] = [];
break;
}
}
}
count += result.length;
}
if (_tag) break;
}
if (!count) {
cursor.delete();
//store.delete(cursor.key);
} else if (changed) {
//await new Promise(resolve => {
cursor.update(arr); //.onsuccess = resolve;
//});
}
cursor.continue();
}
/**
* @param {Array<number|string>} ids
* @return {!Promise<undefined>}
*/
IdxDB.prototype.remove = function (ids) {
const self = this;
if ("object" != typeof ids) {
ids = [ids];
}
return (/** @type {!Promise<undefined>} */Promise.all([self.transaction("map", "readwrite", function (store) {
store.openCursor().onsuccess = function () {
const cursor = this.result;
cursor && handle(cursor, ids);
};
}), self.transaction("ctx", "readwrite", function (store) {
store.openCursor().onsuccess = function () {
const cursor = this.result;
cursor && handle(cursor, ids);
};
}), self.transaction("tag", "readwrite", function (store) {
store.openCursor().onsuccess = function () {
const cursor = this.result;
cursor && handle(cursor, ids, !0);
};
}),
// let filtered = [];
self.transaction("reg", "readwrite", function (store) {
for (let i = 0; i < ids.length; i++) {
store.delete(ids[i]);
}
// return new Promise(resolve => {
// store.openCursor().onsuccess = function(){
// const cursor = this.result;
// if(cursor){
// const id = cursor.value;
// if(ids.includes(id)){
// filtered.push(id);
// cursor.delete();
// }
// cursor.continue();
// }
// else{
// resolve();
// }
// };
// });
})
// ids = filtered;
])
);
};
/**
* @param {IDBRequest|IDBOpenDBRequest} req
* @param {Function=} callback
* @return {!Promise<undefined>}
*/
function promisfy(req, callback) {
return new Promise((resolve, reject) => {
// oncomplete is used for transaction
/** @this {IDBRequest|IDBOpenDBRequest} */
req.onsuccess = req.oncomplete = function () {
callback && callback(this.result);
callback = null;
resolve(this.result);
};
req.onerror = req.onblocked = reject;
req = null;
});
}