194 lines
4.9 KiB
HTML
194 lines
4.9 KiB
HTML
|
<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 selectLast(roomId, amount) {
|
||
|
const tx = this._database.transaction(["timeline"], "readonly");
|
||
|
const store = tx.objectStore("timeline");
|
||
|
const maxKey = new Key();
|
||
|
maxKey.gapKey = Number.MAX_SAFE_INTEGER;
|
||
|
maxKey.eventKey = Number.MAX_SAFE_INTEGER;
|
||
|
const range = IDBKeyRange.upperBound([roomId, maxKey.buffer()]);
|
||
|
const cursor = store.openCursor(range, "prev");
|
||
|
const events = await fetchResults(cursor,
|
||
|
(results) => results.length === amount,
|
||
|
(cursor) => cursor.value);
|
||
|
events.reverse();
|
||
|
return events;
|
||
|
}
|
||
|
|
||
|
async selectFirst(roomId, amount) {
|
||
|
const tx = this._database.transaction(["timeline"], "readonly");
|
||
|
const store = tx.objectStore("timeline");
|
||
|
const minKey = new Key();
|
||
|
const range = IDBKeyRange.lowerBound([roomId, minKey.buffer()]);
|
||
|
const cursor = store.openCursor(range, "next");
|
||
|
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.selectFirst(roomId, 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.selectFirst(roomId, 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>
|