<html> <head><meta charset="utf-8"></head> <body> <ul id="messages"></ul> <script type="text/javascript"> class Key { constructor() { this._keys = new Int32Array(2); } get gapKey() { return this._keys[0]; } set gapKey(value) { this._keys[0] = value; } get eventKey() { return this._keys[1]; } set eventKey(value) { this._keys[1] = value; } buffer() { return this._keys.buffer; } nextKeyWithGap() { const k = new Key(); k.gapKey = this.gapKey + 1; k.eventKey = 0; return k; } nextKey() { const k = new Key(); k.gapKey = this.gapKey; k.eventKey = this.eventKey + 1; return k; } previousKey() { const k = new Key(); k.gapKey = this.gapKey; k.eventKey = this.eventKey - 1; return k; } clone() { const k = new Key(); k.gapKey = this.gapKey; k.eventKey = this.eventKey; return k; } } function reqAsPromise(req) { return new Promise((resolve, reject) => { req.onsuccess = () => resolve(req); req.onerror = (err) => reject(err); }); } function fetchResults(cursor, isDone, resultMapper) { return new Promise((resolve, reject) => { const results = []; cursor.onerror = (event) => { reject(new Error("Query failed: " + event.target.errorCode)); }; // collect results cursor.onsuccess = (event) => { console.log("got a result"); const cursor = event.target.result; if (!cursor) { resolve(results); return; // end of results } results.push(resultMapper(cursor)); if (!isDone(results)) { cursor.continue(); } else { resolve(results); } }; }); } class Storage { constructor(databaseName) { this._databaseName = databaseName; this._database = null; } async open() { const req = window.indexedDB.open(this._databaseName); req.onupgradeneeded = (ev) => { const db = ev.target.result; const oldVersion = ev.oldVersion; this._createStores(db, oldVersion); }; await reqAsPromise(req); this._database = req.result; } _createStores(db) { db.createObjectStore("timeline", {keyPath: ["roomId", "sortKey"]}); } async insert(value) { const tx = this._database.transaction(["timeline"], "readwrite"); const store = tx.objectStore("timeline"); await reqAsPromise(store.add(value)); } async selectAt(roomId, sortKey, amount) { const tx = this._database.transaction(["timeline"], "readonly"); const store = tx.objectStore("timeline"); const range = IDBKeyRange.lowerBound([roomId, sortKey.buffer()]); const cursor = store.openCursor(range); return await fetchResults(cursor, (results) => results.length === amount, (cursor) => cursor.value); } } (async () => { const initialSortKey = new Key(); initialSortKey.gapKey = 1000; const roomId = "!abc:hs.tld"; const storage = new Storage("mysession"); await storage.open(); let records = await storage.selectAt(roomId, initialSortKey, 15); if (!records.length) { // insert first batch backwards, // to see we're not assuming insertion order to sort let sortKey = initialSortKey.clone(); sortKey.eventKey = 10; for (var i = 10; i > 0; i--) { await storage.insert({ roomId, sortKey: sortKey.buffer(), message: `message ${i} before gap` }); sortKey = sortKey.previousKey(); } sortKey = sortKey.nextKeyWithGap(); await storage.insert({ roomId, sortKey: sortKey.buffer(), message: `event to represent gap!` }); for (var i = 1; i <= 10; i++) { sortKey = sortKey.nextKey(); await storage.insert({ roomId, sortKey: sortKey.buffer(), message: `message ${i} after gap` }); } records = await storage.selectAt(roomId, initialSortKey, 15); } console.log(records, "records"); const nodes = records.map(r => { const li = document.createElement("li"); li.appendChild(document.createTextNode(r.message)); return li; }); const parentNode = document.getElementById("messages"); nodes.forEach(n => parentNode.appendChild(n)); })(); </script> </body> </html>