git » sdk » commit a00b0f2

npmify

author Stephen Paul Weber
2024-06-19 15:08:47 UTC
committer Stephen Paul Weber
2024-06-19 15:08:47 UTC
parent 3cffc8ab7c6dcc816548f5973d33e9e350c0aa04

npmify

.github/workflows/build.yml +11 -6
.gitignore +7 -0
Makefile +15 -7
browser.hxml => js.hxml +1 -1
npm/index.ts +27 -0
npm/package.json +21 -0
snikket/Chat.hx +0 -9
snikket/Message.hx +0 -18
snikket/persistence/browser.js +476 -471

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bf1568f..808ddb5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -45,9 +45,14 @@ jobs:
         haxelib --quiet install sha
         haxelib --quiet install thenshim
         haxelib --quiet install hxcpp
-        haxelib --quiet install hxtsdgen
+        haxelib --quiet git hxtsdgen https://github.com/singpolyma/hxtsdgen
         haxelib --quiet install utest
 
+    - name: NPM Dependencies
+      run: |
+        cd npm
+        npm i
+
     - name: Tests
       run: make test
 
@@ -62,12 +67,12 @@ jobs:
           libsnikket.so
           cpp/snikket.h
 
+    - name: NPM Tarball
+      run: tar -cjf npm.tar.gz npm/
+
     - name: JS Artifact
       uses: actions/upload-artifact@v4
       with:
-        name: browser.js
+        name: npm.tar.gz
         path: |
-          browser.js
-          browser.haxe.d.ts
-          browser.haxe-enums.ts
-            
+          npm.tar.gz
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bed90d4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+npm/package-lock.json
+npm/*.d.ts
+npm/snikket.js
+npm/snikket-enums.ts
+npm/snikket-enums.js
+npm/index.js
+node_modules
diff --git a/Makefile b/Makefile
index 59caa45..87de915 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,25 @@
 HAXE_PATH=$$HOME/Software/haxe-4.3.1/hxnodejs/12,1,0/src
 
-.PHONY: all test cpp/output.dso browser.js
+.PHONY: all test cpp/output.dso npm/snikket.js
 
-all: browser.js libsnikket.so
+all: npm libsnikket.so
 
 test:
 	haxe test.hxml
 
-browser.js:
-	haxe browser.hxml
-	echo "var exports = {};" > browser.js
-	cat snikket/persistence/*.js >> browser.js
-	echo "export const { snikket } = exports;" >> browser.js
+npm/snikket.js:
+	haxe js.hxml
+	sed -i 's/import { snikket }/import { snikket as enums }/' npm/snikket.d.ts
+	sed -i 's/snikket\.UiState/enums.UiState/g' npm/snikket.d.ts
+	sed -i 's/snikket\.MessageStatus/enums.MessageStatus/g' npm/snikket.d.ts
+	sed -i 's/snikket\.MessageDirection/enums.MessageDirection/g' npm/snikket.d.ts
+	sed -i '1ivar exports = {};' npm/snikket.js
+	echo "export const snikket = exports.snikket;" >> npm/snikket.js
+	cd npm && npx tsc --esModuleInterop --lib esnext,dom --target esnext --preserveConstEnums -d index.ts
+	sed -i '1iimport { snikket as enums } from "./snikket-enums";' npm/index.js
+
+npm: npm/snikket.js snikket/persistence/browser.js
+	cp snikket/persistence/browser.js npm
 
 cpp/output.dso:
 	haxe cpp.hxml
diff --git a/browser.hxml b/js.hxml
similarity index 90%
rename from browser.hxml
rename to js.hxml
index 7d2de8b..05d4971 100644
--- a/browser.hxml
+++ b/js.hxml
@@ -11,4 +11,4 @@ snikket.Push
 -D js-es=6
 -D hxtsdgen_enums_ts
 -D hxtsdgen_namespaced
---js browser.haxe.js
+--js npm/snikket.js
diff --git a/npm/index.ts b/npm/index.ts
new file mode 100644
index 0000000..5a964cb
--- /dev/null
+++ b/npm/index.ts
@@ -0,0 +1,27 @@
+import browserp from "./browser";
+import { snikket as enums } from "./snikket-enums";
+import { snikket } from "./snikket";
+
+// TODO: should we autogenerate this?
+export import AvailableChat = snikket.AvailableChat;
+export import Caps = snikket.Caps;
+export import Channel = snikket.Channel;
+export import Chat = snikket.Chat;
+export import ChatAttachment = snikket.ChatAttachment;
+export import ChatMessage = snikket.ChatMessage;
+export import Client = snikket.Client;
+export import DirectChat = snikket.DirectChat;
+export import Identicon = snikket.Identicon;
+export import Identity = snikket.Identity;
+export import Notification = snikket.Notification;
+export import SerializedChat = snikket.SerializedChat;
+export import jingle = snikket.jingle;
+
+export import UiState = enums.UiState;
+export import MessageStatus = enums.MessageStatus;
+export import MessageDirection = enums.MessageDirection;
+
+export namespace persistence {
+	 export import browser = browserp;
+	 export import Dummy = snikket.persistence.Dummy;
+}
diff --git a/npm/package.json b/npm/package.json
new file mode 100644
index 0000000..7068fb9
--- /dev/null
+++ b/npm/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "snikket-sdk",
+  "version": "0.0.0",
+  "description": "Chat SDK",
+  "main": "index.js",
+  "files": [
+    "*.js",
+    "*.ts"
+  ],
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "sasl-scram-sha-1": "github:singpolyma/js-sasl-scram-sha-1"
+  },
+  "devDependencies": {
+    "typescript": "^5.4.5"
+  }
+}
diff --git a/snikket/Chat.hx b/snikket/Chat.hx
index d417d96..4e938f0 100644
--- a/snikket/Chat.hx
+++ b/snikket/Chat.hx
@@ -23,15 +23,6 @@ enum abstract UiState(Int) {
 	var Closed; // Archived
 }
 
-#if js
-@:expose("UiState")
-class UiStateImpl {
-	static public final Pinned = UiState.Pinned;
-	static public final Open = UiState.Open;
-	static public final Closed = UiState.Closed;
-}
-#end
-
 #if cpp
 @:build(HaxeCBridge.expose())
 @:build(HaxeSwiftBridge.expose())
diff --git a/snikket/Message.hx b/snikket/Message.hx
index bee5488..66f66d2 100644
--- a/snikket/Message.hx
+++ b/snikket/Message.hx
@@ -7,14 +7,6 @@ enum abstract MessageDirection(Int) {
 	var MessageSent;
 }
 
-#if js
-@:expose("MessageDirection")
-class MessageDirectionImpl {
-	static public final MessageReceived = MessageDirection.MessageReceived;
-	static public final MessageSent = MessageDirection.MessageSent;
-}
-#end
-
 enum abstract MessageStatus(Int) {
 	var MessagePending; // Message is waiting in client for sending
 	var MessageDeliveredToServer; // Server acknowledged receipt of the message
@@ -22,16 +14,6 @@ enum abstract MessageStatus(Int) {
 	var MessageFailedToSend; // There was an error sending this message
 }
 
-#if js
-@:expose("MessageStatus")
-class MessageStatusImpl {
-	static public final MessagePending = MessageStatus.MessagePending;
-	static public final MessageDeliveredToServer = MessageStatus.MessageDeliveredToServer;
-	static public final MessageDeliveredToDevice = MessageStatus.MessageDeliveredToDevice;
-	static public final MessageFailedToSend = MessageStatus.MessageFailedToSend;
-}
-#end
-
 enum MessageStanza {
 	ErrorMessageStanza(stanza: Stanza);
 	ChatMessageStanza(message: ChatMessage);
diff --git a/snikket/persistence/browser.js b/snikket/persistence/browser.js
index e800db3..d4ac934 100644
--- a/snikket/persistence/browser.js
+++ b/snikket/persistence/browser.js
@@ -1,529 +1,534 @@
 // This example persistence driver is written in JavaScript
 // so that SDK users can easily see how to write their own
 
-exports.snikket.persistence = {
-	browser: (dbname) => {
-		var db = null;
-		function openDb(version) {
-			var dbOpenReq = indexedDB.open(dbname, version);
-			dbOpenReq.onerror = console.error;
-			dbOpenReq.onupgradeneeded = (event) => {
-				const upgradeDb = event.target.result;
-				if (!db.objectStoreNames.contains("messages")) {
-					const messages = upgradeDb.createObjectStore("messages", { keyPath: ["account", "serverId", "serverIdBy", "localId"] });
-					messages.createIndex("chats", ["account", "chatId", "timestamp"]);
-					messages.createIndex("localId", ["account", "localId", "chatId"]);
-					messages.createIndex("accounts", ["account", "timestamp"]);
-				}
-				if (!db.objectStoreNames.contains("keyvaluepairs")) {
-					upgradeDb.createObjectStore("keyvaluepairs");
-				}
-				if (!db.objectStoreNames.contains("chats")) {
-					upgradeDb.createObjectStore("chats", { keyPath: ["account", "chatId"] });
-				}
-				if (!db.objectStoreNames.contains("services")) {
-					upgradeDb.createObjectStore("services", { keyPath: ["account", "serviceId"] });
-				}
-				if (!db.objectStoreNames.contains("reactions")) {
-					const reactions = upgradeDb.createObjectStore("reactions", { keyPath: ["account", "chatId", "senderId", "updateId"] });
-					reactions.createIndex("senders", ["account", "chatId", "messageId", "senderId", "timestamp"]);
-				}
-			};
-			dbOpenReq.onsuccess = (event) => {
-				db = event.target.result;
-				if (!db.objectStoreNames.contains("messages") || !db.objectStoreNames.contains("keyvaluepairs") || !db.objectStoreNames.contains("chats") || !db.objectStoreNames.contains("services") || !db.objectStoreNames.contains("reactions")) {
-					db.close();
-					openDb(db.version + 1);
-					return;
-				}
-			};
-		}
-		openDb();
+import { snikket as enums } from "./snikket-enums";
+import { snikket } from "./snikket";
+
+const browser = (dbname) => {
+	var db = null;
+	function openDb(version) {
+		var dbOpenReq = indexedDB.open(dbname, version);
+		dbOpenReq.onerror = console.error;
+		dbOpenReq.onupgradeneeded = (event) => {
+			const upgradeDb = event.target.result;
+			if (!db.objectStoreNames.contains("messages")) {
+				const messages = upgradeDb.createObjectStore("messages", { keyPath: ["account", "serverId", "serverIdBy", "localId"] });
+				messages.createIndex("chats", ["account", "chatId", "timestamp"]);
+				messages.createIndex("localId", ["account", "localId", "chatId"]);
+				messages.createIndex("accounts", ["account", "timestamp"]);
+			}
+			if (!db.objectStoreNames.contains("keyvaluepairs")) {
+				upgradeDb.createObjectStore("keyvaluepairs");
+			}
+			if (!db.objectStoreNames.contains("chats")) {
+				upgradeDb.createObjectStore("chats", { keyPath: ["account", "chatId"] });
+			}
+			if (!db.objectStoreNames.contains("services")) {
+				upgradeDb.createObjectStore("services", { keyPath: ["account", "serviceId"] });
+			}
+			if (!db.objectStoreNames.contains("reactions")) {
+				const reactions = upgradeDb.createObjectStore("reactions", { keyPath: ["account", "chatId", "senderId", "updateId"] });
+				reactions.createIndex("senders", ["account", "chatId", "messageId", "senderId", "timestamp"]);
+			}
+		};
+		dbOpenReq.onsuccess = (event) => {
+			db = event.target.result;
+			if (!db.objectStoreNames.contains("messages") || !db.objectStoreNames.contains("keyvaluepairs") || !db.objectStoreNames.contains("chats") || !db.objectStoreNames.contains("services") || !db.objectStoreNames.contains("reactions")) {
+				db.close();
+				openDb(db.version + 1);
+				return;
+			}
+		};
+	}
+	openDb();
 
-		var cache = null;
-		caches.open(dbname).then((c) => cache = c);
+	var cache = null;
+	caches.open(dbname).then((c) => cache = c);
 
-		function mkNiUrl(hashAlgorithm, hashBytes) {
-			const b64url = btoa(Array.from(new Uint8Array(hashBytes), (x) => String.fromCodePoint(x)).join("")).replace(/\+/, "-").replace(/\//, "_").replace(/=/, "");
-			return "/.well-known/ni/" + hashAlgorithm + "/" + b64url;
-		}
+	function mkNiUrl(hashAlgorithm, hashBytes) {
+		const b64url = btoa(Array.from(new Uint8Array(hashBytes), (x) => String.fromCodePoint(x)).join("")).replace(/\+/, "-").replace(/\//, "_").replace(/=/, "");
+		return "/.well-known/ni/" + hashAlgorithm + "/" + b64url;
+	}
 
-		function promisifyRequest(request) {
-			return new Promise((resolve, reject) => {
-				request.oncomplete = request.onsuccess = () => resolve(request.result);
-				request.onabort = request.onerror = () => reject(request.error);
-			});
-		}
+	function promisifyRequest(request) {
+		return new Promise((resolve, reject) => {
+			request.oncomplete = request.onsuccess = () => resolve(request.result);
+			request.onabort = request.onerror = () => reject(request.error);
+		});
+	}
 
-		async function hydrateMessage(value) {
-			if (!value) return null;
+	async function hydrateMessage(value) {
+		if (!value) return null;
+
+		const tx = db.transaction(["messages"], "readonly");
+		const store = tx.objectStore("messages");
+		let replyToMessage = value.replyToMessage && await hydrateMessage((await promisifyRequest(store.openCursor(IDBKeyRange.only(value.replyToMessage))))?.value);
+
+		const message = new snikket.ChatMessage();
+		message.localId = value.localId ? value.localId : null;
+		message.serverId = value.serverId ? value.serverId : null;
+		message.serverIdBy = value.serverIdBy ? value.serverIdBy : null;
+		message.syncPoint = !!value.syncPoint;
+      message.direction = value.direction;
+      message.status = value.status;
+		message.timestamp = value.timestamp && value.timestamp.toISOString();
+		message.to = value.to && snikket.JID.parse(value.to);
+		message.from = value.from && snikket.JID.parse(value.from);
+		message.sender = value.sender && snikket.JID.parse(value.sender);
+		message.recipients = value.recipients.map((r) => snikket.JID.parse(r));
+		message.replyTo = value.replyTo.map((r) => snikket.JID.parse(r));
+		message.replyToMessage = replyToMessage;
+		message.threadId = value.threadId;
+		message.attachments = value.attachments;
+		message.reactions = value.reactions;
+		message.text = value.text;
+		message.lang = value.lang;
+		message.isGroupchat = value.isGroupchat || value.groupchat;
+		message.versions = await Promise.all((value.versions || []).map(hydrateMessage));
+		message.payloads = (value.payloads || []).map(snikket.Stanza.parse);
+		return message;
+	}
 
-			const tx = db.transaction(["messages"], "readonly");
-			const store = tx.objectStore("messages");
-			let replyToMessage = value.replyToMessage && await hydrateMessage((await promisifyRequest(store.openCursor(IDBKeyRange.only(value.replyToMessage))))?.value);
-
-			const message = new snikket.ChatMessage();
-			message.localId = value.localId ? value.localId : null;
-			message.serverId = value.serverId ? value.serverId : null;
-			message.serverIdBy = value.serverIdBy ? value.serverIdBy : null;
-			message.syncPoint = !!value.syncPoint;
-			message.timestamp = value.timestamp && value.timestamp.toISOString();
-			message.to = value.to && snikket.JID.parse(value.to);
-			message.from = value.from && snikket.JID.parse(value.from);
-			message.sender = value.sender && snikket.JID.parse(value.sender);
-			message.recipients = value.recipients.map((r) => snikket.JID.parse(r));
-			message.replyTo = value.replyTo.map((r) => snikket.JID.parse(r));
-			message.replyToMessage = replyToMessage;
-			message.threadId = value.threadId;
-			message.attachments = value.attachments;
-			message.reactions = value.reactions;
-			message.text = value.text;
-			message.lang = value.lang;
-			message.isGroupchat = value.isGroupchat || value.groupchat;
-			message.versions = await Promise.all((value.versions || []).map(hydrateMessage));
-			message.payloads = (value.payloads || []).map(snikket.Stanza.parse);
-			return message;
+	function serializeMessage(account, message) {
+		return {
+			...message,
+			serverId: message.serverId || "",
+			serverIdBy: message.serverIdBy || "",
+			localId: message.localId || "",
+			syncPoint: !!message.syncPoint,
+			account: account,
+			chatId: message.chatId(),
+			to: message.to?.asString(),
+			from: message.from?.asString(),
+			sender: message.sender?.asString(),
+			recipients: message.recipients.map((r) => r.asString()),
+			replyTo: message.replyTo.map((r) => r.asString()),
+			timestamp: new Date(message.timestamp),
+			replyToMessage: message.replyToMessage && [account, message.replyToMessage.serverId || "", message.replyToMessage.serverIdBy || "", message.replyToMessage.localId || ""],
+			versions: message.versions.map((m) => serializeMessage(account, m)),
+			payloads: message.payloads.map((p) => p.toString()),
 		}
+	}
 
-		function serializeMessage(account, message) {
-			return {
-				...message,
-				serverId: message.serverId || "",
-				serverIdBy: message.serverIdBy || "",
-				localId: message.localId || "",
-				syncPoint: !!message.syncPoint,
-				account: account,
-				chatId: message.chatId(),
-				to: message.to?.asString(),
-				from: message.from?.asString(),
-				sender: message.sender?.asString(),
-				recipients: message.recipients.map((r) => r.asString()),
-				replyTo: message.replyTo.map((r) => r.asString()),
-				timestamp: new Date(message.timestamp),
-				replyToMessage: message.replyToMessage && [account, message.replyToMessage.serverId || "", message.replyToMessage.serverIdBy || "", message.replyToMessage.localId || ""],
-				versions: message.versions.map((m) => serializeMessage(account, m)),
-				payloads: message.payloads.map((p) => p.toString()),
+	function correctMessage(account, message, result) {
+		// Newest (by timestamp) version wins for head
+		const newVersions = message.versions.length < 1 ? [message] : message.versions;
+		const storedVersions = result.value.versions || [];
+		// TODO: dedupe? There shouldn't be dupes...
+		const versions = (storedVersions.length < 1 ? [result.value] : storedVersions).concat(newVersions.map((nv) => serializeMessage(account, nv))).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
+		const head = {...versions[0]};
+		// Can't change primary key
+		head.serverIdBy = result.value.serverIdBy;
+		head.serverId = result.value.serverId;
+		head.localId = result.value.localId;
+		head.timestamp = result.value.timestamp; // Edited version is not newer
+		head.versions = versions;
+		head.reactions = result.value.reactions; // Preserve these, edit doesn't touch them
+		result.update(head);
+		return head;
+	}
+
+	function setReactions(reactionsMap, sender, reactions) {
+		for (const [reaction, senders] of reactionsMap) {
+			if (!reactions.includes(reaction) && senders.includes(sender)) {
+				if (senders.length === 1) {
+					reactionsMap.delete(reaction);
+				} else {
+					reactionsMap.set(reaction, senders.filter((asender) => asender != sender));
+				}
 			}
 		}
-
-		function correctMessage(account, message, result) {
-			// Newest (by timestamp) version wins for head
-			const newVersions = message.versions.length < 1 ? [message] : message.versions;
-			const storedVersions = result.value.versions || [];
-			// TODO: dedupe? There shouldn't be dupes...
-			const versions = (storedVersions.length < 1 ? [result.value] : storedVersions).concat(newVersions.map((nv) => serializeMessage(account, nv))).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
-			const head = {...versions[0]};
-			// Can't change primary key
-			head.serverIdBy = result.value.serverIdBy;
-			head.serverId = result.value.serverId;
-			head.localId = result.value.localId;
-			head.timestamp = result.value.timestamp; // Edited version is not newer
-			head.versions = versions;
-			head.reactions = result.value.reactions; // Preserve these, edit doesn't touch them
-			result.update(head);
-			return head;
+		for (const reaction of reactions) {
+			reactionsMap.set(reaction, [...new Set([...reactionsMap.get(reaction) || [], sender])].sort());
 		}
+		return reactionsMap;
+	}
 
-		function setReactions(reactionsMap, sender, reactions) {
-			for (const [reaction, senders] of reactionsMap) {
-				if (!reactions.includes(reaction) && senders.includes(sender)) {
-					if (senders.length === 1) {
-						reactionsMap.delete(reaction);
-					} else {
-						reactionsMap.set(reaction, senders.filter((asender) => asender != sender));
-					}
+	return {
+		lastId: function(account, jid, callback) {
+			const tx = db.transaction(["messages"], "readonly");
+			const store = tx.objectStore("messages");
+			var cursor = null;
+			if (jid === null) {
+				cursor = store.index("accounts").openCursor(
+					IDBKeyRange.bound([account], [account, []]),
+					"prev"
+				);
+			} else {
+				cursor = store.index("chats").openCursor(
+					IDBKeyRange.bound([account, jid], [account, jid, []]),
+					"prev"
+				);
+			}
+			cursor.onsuccess = (event) => {
+				if (!event.target.result || (event.target.result.value.syncPoint && event.target.result.value.serverId && (jid || event.target.result.value.serverIdBy === account))) {
+					callback(event.target.result ? event.target.result.value.serverId : null);
+				} else {
+					event.target.result.continue();
 				}
 			}
-			for (const reaction of reactions) {
-				reactionsMap.set(reaction, [...new Set([...reactionsMap.get(reaction) || [], sender])].sort());
+			cursor.onerror = (event) => {
+				console.error(event);
+				callback(null);
 			}
-			return reactionsMap;
-		}
+		},
 
-		return {
-			lastId: function(account, jid, callback) {
-				const tx = db.transaction(["messages"], "readonly");
-				const store = tx.objectStore("messages");
-				var cursor = null;
-				if (jid === null) {
-					cursor = store.index("accounts").openCursor(
-						IDBKeyRange.bound([account], [account, []]),
-						"prev"
-					);
-				} else {
-					cursor = store.index("chats").openCursor(
-						IDBKeyRange.bound([account, jid], [account, jid, []]),
-						"prev"
-					);
-				}
-				cursor.onsuccess = (event) => {
-					if (!event.target.result || (event.target.result.value.syncPoint && event.target.result.value.serverId && (jid || event.target.result.value.serverIdBy === account))) {
-						callback(event.target.result ? event.target.result.value.serverId : null);
-					} else {
-						event.target.result.continue();
-					}
-				}
-				cursor.onerror = (event) => {
-					console.error(event);
-					callback(null);
-				}
-			},
+		storeChat: function(account, chat) {
+			const tx = db.transaction(["chats"], "readwrite");
+			const store = tx.objectStore("chats");
 
-			storeChat: function(account, chat) {
-				const tx = db.transaction(["chats"], "readwrite");
-				const store = tx.objectStore("chats");
+			store.put({
+				account: account,
+				chatId: chat.chatId,
+				trusted: chat.trusted,
+				avatarSha1: chat.avatarSha1,
+				presence: new Map([...chat.presence.entries()].map(([k, p]) => [k, { caps: p.caps?.ver(), mucUser: p.mucUser?.toString() }])),
+				displayName: chat.displayName,
+				uiState: chat.uiState,
+				extensions: chat.extensions?.toString(),
+				disco: chat.disco,
+				class: chat instanceof snikket.DirectChat ? "DirectChat" : (chat instanceof snikket.Channel ? "Channel" : "Chat")
+			});
+		},
 
-				store.put({
-					account: account,
-					chatId: chat.chatId,
-					trusted: chat.trusted,
-					avatarSha1: chat.avatarSha1,
-					presence: new Map([...chat.presence.entries()].map(([k, p]) => [k, { caps: p.caps?.ver(), mucUser: p.mucUser?.toString() }])),
-					displayName: chat.displayName,
-					uiState: chat.uiState,
-					extensions: chat.extensions?.toString(),
-					disco: chat.disco,
-					class: chat instanceof snikket.DirectChat ? "DirectChat" : (chat instanceof snikket.Channel ? "Channel" : "Chat")
-				});
-			},
-
-			getChats: function(account, callback) {
-				(async () => {
-					const tx = db.transaction(["chats"], "readonly");
-					const store = tx.objectStore("chats");
-					const range = IDBKeyRange.bound([account], [account, []]);
-					const result = await promisifyRequest(store.getAll(range));
-					return await Promise.all(result.map(async (r) => new snikket.SerializedChat(
-						r.chatId,
-						r.trusted,
-						r.avatarSha1,
-						new Map(await Promise.all((r.presence instanceof Map ? [...r.presence.entries()] : Object.entries(r.presence)).map(
-							async ([k, p]) => [k, new snikket.Presence(p.caps && await new Promise((resolve) => this.getCaps(p.caps, resolve)), p.mucUser && snikket.Stanza.parse(p.mucUser))]
-						))),
-						r.displayName,
-						r.uiState,
-						r.extensions,
-						r.disco,
-						r.class
-					)));
-				})().then(callback);
-			},
-
-			getChatsUnreadDetails: function(account, chatsArray, callback) {
-				const tx = db.transaction(["messages"], "readonly");
-				const store = tx.objectStore("messages");
+		getChats: function(account, callback) {
+			(async () => {
+				const tx = db.transaction(["chats"], "readonly");
+				const store = tx.objectStore("chats");
+				const range = IDBKeyRange.bound([account], [account, []]);
+				const result = await promisifyRequest(store.getAll(range));
+				return await Promise.all(result.map(async (r) => new snikket.SerializedChat(
+					r.chatId,
+					r.trusted,
+					r.avatarSha1,
+					new Map(await Promise.all((r.presence instanceof Map ? [...r.presence.entries()] : Object.entries(r.presence)).map(
+						async ([k, p]) => [k, new snikket.Presence(p.caps && await new Promise((resolve) => this.getCaps(p.caps, resolve)), p.mucUser && snikket.Stanza.parse(p.mucUser))]
+					))),
+					r.displayName,
+					r.uiState,
+					r.extensions,
+					r.disco,
+					r.class
+				)));
+			})().then(callback);
+		},
+
+		getChatsUnreadDetails: function(account, chatsArray, callback) {
+			const tx = db.transaction(["messages"], "readonly");
+			const store = tx.objectStore("messages");
 
-				const cursor = store.index("accounts").openCursor(
-					IDBKeyRange.bound([account], [account, []]),
-					"prev"
-				);
-				const chats = {};
-				chatsArray.forEach((chat) => chats[chat.chatId] = chat);
-				const result = {};
-				var rowCount = 0;
-				cursor.onsuccess = (event) => {
-					if (event.target.result && rowCount < 40000) {
-						rowCount++;
-						const value = event.target.result.value;
-						if (result[value.chatId]) {
-							if (!result[value.chatId].foundAll) {
-								const readUpTo = chats[value.chatId]?.readUpTo();
-								if (readUpTo === value.serverId || readUpTo === value.localId || value.direction == "MessageSent") {
-									result[value.chatId].foundAll = true;
-								} else {
-									result[value.chatId] = result[value.chatId].then((details) => { details.unreadCount++; return details; });
-								}
-							}
-						} else {
+			const cursor = store.index("accounts").openCursor(
+				IDBKeyRange.bound([account], [account, []]),
+				"prev"
+			);
+			const chats = {};
+			chatsArray.forEach((chat) => chats[chat.chatId] = chat);
+			const result = {};
+			var rowCount = 0;
+			cursor.onsuccess = (event) => {
+				if (event.target.result && rowCount < 40000) {
+					rowCount++;
+					const value = event.target.result.value;
+					if (result[value.chatId]) {
+						if (!result[value.chatId].foundAll) {
 							const readUpTo = chats[value.chatId]?.readUpTo();
-							const haveRead = readUpTo === value.serverId || readUpTo === value.localId || value.direction == "MessageSent";
-							result[value.chatId] = hydrateMessage(value).then((m) => ({ chatId: value.chatId, message: m, unreadCount: haveRead ? 0 : 1, foundAll: haveRead }));
+							if (readUpTo === value.serverId || readUpTo === value.localId || value.direction == enums.MessageDirection.MessageSent) {
+								result[value.chatId].foundAll = true;
+							} else {
+								result[value.chatId] = result[value.chatId].then((details) => { details.unreadCount++; return details; });
+							}
 						}
-						event.target.result.continue();
 					} else {
-						Promise.all(Object.values(result)).then(callback);
+						const readUpTo = chats[value.chatId]?.readUpTo();
+						const haveRead = readUpTo === value.serverId || readUpTo === value.localId || value.direction == enums.MessageDirection.MessageSent;
+						result[value.chatId] = hydrateMessage(value).then((m) => ({ chatId: value.chatId, message: m, unreadCount: haveRead ? 0 : 1, foundAll: haveRead }));
 					}
+					event.target.result.continue();
+				} else {
+					Promise.all(Object.values(result)).then(callback);
 				}
-				cursor.onerror = (event) => {
-					console.error(event);
-					callback([]);
-				}
-			},
+			}
+			cursor.onerror = (event) => {
+				console.error(event);
+				callback([]);
+			}
+		},
 
-			getMessage: function(account, chatId, serverId, localId, callback) {
-				const tx = db.transaction(["messages"], "readonly");
+		getMessage: function(account, chatId, serverId, localId, callback) {
+			const tx = db.transaction(["messages"], "readonly");
+			const store = tx.objectStore("messages");
+			(async function() {
+				let result;
+				if (serverId) {
+					result = await promisifyRequest(store.openCursor(IDBKeyRange.bound([account, serverId], [account, serverId, []])));
+				} else {
+					result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, localId, chatId])));
+				}
+				if (!result || !result.value) return null;
+				const message = result.value;
+				return await hydrateMessage(message);
+			})().then(callback);
+		},
+
+		storeReaction: function(account, update, callback) {
+			(async function() {
+				const tx = db.transaction(["messages", "reactions"], "readwrite");
 				const store = tx.objectStore("messages");
-				(async function() {
-					let result;
-					if (serverId) {
-						result = await promisifyRequest(store.openCursor(IDBKeyRange.bound([account, serverId], [account, serverId, []])));
-					} else {
-						result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, localId, chatId])));
-					}
-					if (!result || !result.value) return null;
-					const message = result.value;
-					return await hydrateMessage(message);
-				})().then(callback);
-			},
-
-			storeReaction: function(account, update, callback) {
-				(async function() {
-					const tx = db.transaction(["messages", "reactions"], "readwrite");
-					const store = tx.objectStore("messages");
-					const reactionStore = tx.objectStore("reactions");
-					let result;
-					if (update.serverId) {
-						result = await promisifyRequest(store.openCursor(IDBKeyRange.bound([account, update.serverId], [account, update.serverId, []])));
-					} else {
-						result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, update.localId, update.chatId])));
-					}
-					await promisifyRequest(reactionStore.put({...update, messageId: update.serverId || update.localId, timestamp: new Date(update.timestamp), account: account}));
-					if (!result || !result.value) {
-						return null;
+				const reactionStore = tx.objectStore("reactions");
+				let result;
+				if (update.serverId) {
+					result = await promisifyRequest(store.openCursor(IDBKeyRange.bound([account, update.serverId], [account, update.serverId, []])));
+				} else {
+					result = await promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, update.localId, update.chatId])));
+				}
+				await promisifyRequest(reactionStore.put({...update, messageId: update.serverId || update.localId, timestamp: new Date(update.timestamp), account: account}));
+				if (!result || !result.value) {
+					return null;
+				}
+				const message = result.value;
+				const lastFromSender = promisifyRequest(reactionStore.index("senders").openCursor(IDBKeyRange.bound(
+					[account, update.chatId, update.serverId || update.localId, update.senderId],
+					[account, update.chatId, update.serverId || update.localId, update.senderId, []]
+				), "prev"));
+				if (lastFromSender?.value && lastFromSender.value.timestamp > new Date(update.timestamp)) return;
+				setReactions(message.reactions, update.senderId, update.reactions);
+				store.put(message);
+				return await hydrateMessage(message);
+			})().then(callback);
+		},
+
+		storeMessage: function(account, message, callback) {
+			if (!message.chatId()) throw "Cannot store a message with no chatId";
+			if (!message.serverId && !message.localId) throw "Cannot store a message with no id";
+			if (!message.serverId && message.isIncoming()) throw "Cannot store an incoming message with no server id";
+			if (message.serverId && !message.serverIdBy) throw "Cannot store a message with a server id and no by";
+			new Promise((resolve) =>
+				// Hydrate reply stubs
+				message.replyToMessage && !message.replyToMessage.serverIdBy ? this.getMessage(account, message.chatId(), message.replyToMessage?.serverId, message.replyToMessage?.localId, resolve) : resolve(message.replyToMessage)
+			).then((replyToMessage) => {
+				message.replyToMessage = replyToMessage;
+				const tx = db.transaction(["messages", "reactions"], "readwrite");
+				const store = tx.objectStore("messages");
+				return promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, message.localId || [], message.chatId()]))).then((result) => {
+					if (result?.value && !message.isIncoming() && result?.value.direction === enums.MessageDirection.MessageSent) {
+						// Duplicate, we trust our own sent ids
+						return promisifyRequest(result.delete());
+					} else if (result?.value && result.value.sender == message.senderId() && (message.versions.length > 0 || (result.value.versions || []).length > 0)) {
+						hydrateMessage(correctMessage(account, message, result)).then(callback);
+						return true;
 					}
-					const message = result.value;
-					const lastFromSender = promisifyRequest(reactionStore.index("senders").openCursor(IDBKeyRange.bound(
-						[account, update.chatId, update.serverId || update.localId, update.senderId],
-						[account, update.chatId, update.serverId || update.localId, update.senderId, []]
-					), "prev"));
-					if (lastFromSender?.value && lastFromSender.value.timestamp > new Date(update.timestamp)) return;
-					setReactions(message.reactions, update.senderId, update.reactions);
-					store.put(message);
-					return await hydrateMessage(message);
-				})().then(callback);
-			},
-
-			storeMessage: function(account, message, callback) {
-				if (!message.chatId()) throw "Cannot store a message with no chatId";
-				if (!message.serverId && !message.localId) throw "Cannot store a message with no id";
-				if (!message.serverId && message.isIncoming()) throw "Cannot store an incoming message with no server id";
-				if (message.serverId && !message.serverIdBy) throw "Cannot store a message with a server id and no by";
-				new Promise((resolve) =>
-					// Hydrate reply stubs
-					message.replyToMessage && !message.replyToMessage.serverIdBy ? this.getMessage(account, message.chatId(), message.replyToMessage?.serverId, message.replyToMessage?.localId, resolve) : resolve(message.replyToMessage)
-				).then((replyToMessage) => {
-					message.replyToMessage = replyToMessage;
-					const tx = db.transaction(["messages", "reactions"], "readwrite");
-					const store = tx.objectStore("messages");
-					return promisifyRequest(store.index("localId").openCursor(IDBKeyRange.only([account, message.localId || [], message.chatId()]))).then((result) => {
-						if (result?.value && !message.isIncoming() && result?.value.direction === "MessageSent") {
-							// Duplicate, we trust our own sent ids
-							return promisifyRequest(result.delete());
-						} else if (result?.value && result.value.sender == message.senderId() && (message.versions.length > 0 || (result.value.versions || []).length > 0)) {
-							hydrateMessage(correctMessage(account, message, result)).then(callback);
-							return true;
-						}
-					}).then((done) => {
-						if (!done) {
-							// There may be reactions already if we are paging backwards
-							const cursor = tx.objectStore("reactions").index("senders").openCursor(IDBKeyRange.bound([account, message.chatId(), (message.isGroupchat ? message.serverId : message.localId) || ""], [account, message.chatId(), (message.isGroupchat ? message.serverId : message.localId) || "", []]), "prev");
-							const reactions = new Map();
-							const reactionTimes = new Map();
-							cursor.onsuccess = (event) => {
-								if (event.target.result && event.target.result.value) {
-									const time = reactionTimes.get(event.target.result.senderId);
-									if (!time || time < event.target.result.value.timestamp) {
-										setReactions(reactions, event.target.result.value.senderId, event.target.result.value.reactions);
-										reactionTimes.set(event.target.result.value.senderId, event.target.result.value.timestamp);
-									}
-									event.target.result.continue();
-								} else {
-									message.reactions = reactions;
-									store.put(serializeMessage(account, message));
-									callback(message);
+				}).then((done) => {
+					if (!done) {
+						// There may be reactions already if we are paging backwards
+						const cursor = tx.objectStore("reactions").index("senders").openCursor(IDBKeyRange.bound([account, message.chatId(), (message.isGroupchat ? message.serverId : message.localId) || ""], [account, message.chatId(), (message.isGroupchat ? message.serverId : message.localId) || "", []]), "prev");
+						const reactions = new Map();
+						const reactionTimes = new Map();
+						cursor.onsuccess = (event) => {
+							if (event.target.result && event.target.result.value) {
+								const time = reactionTimes.get(event.target.result.senderId);
+								if (!time || time < event.target.result.value.timestamp) {
+									setReactions(reactions, event.target.result.value.senderId, event.target.result.value.reactions);
+									reactionTimes.set(event.target.result.value.senderId, event.target.result.value.timestamp);
 								}
-							};
-							cursor.onerror = console.error;
-						}
-					});
-				});
-			},
-
-			updateMessageStatus: function(account, localId, status, callback) {
-				const tx = db.transaction(["messages"], "readwrite");
-				const store = tx.objectStore("messages");
-				promisifyRequest(store.index("localId").openCursor(IDBKeyRange.bound([account, localId], [account, localId, []]))).then((result) => {
-					if (result?.value && result.value.direction === "MessageSent" && result.value.status !== "MessageDeliveredToDevice") {
-						const newStatus = { ...result.value, status: status };
-						result.update(newStatus);
-						hydrateMessage(newStatus).then(callback);
+								event.target.result.continue();
+							} else {
+								message.reactions = reactions;
+								store.put(serializeMessage(account, message));
+								callback(message);
+							}
+						};
+						cursor.onerror = console.error;
 					}
 				});
-			},
-
-			getMessages: function(account, chatId, beforeId, beforeTime, callback) {
-				const beforeDate = beforeTime ? new Date(beforeTime) : [];
-				const tx = db.transaction(["messages"], "readonly");
-				const store = tx.objectStore("messages");
-				const cursor = store.index("chats").openCursor(
-					IDBKeyRange.bound([account, chatId], [account, chatId, beforeDate]),
-					"prev"
-				);
-				const result = [];
-				cursor.onsuccess = (event) => {
-					if (event.target.result && result.length < 50) {
-						const value = event.target.result.value;
-						if (value.serverId === beforeId || (value.timestamp && value.timestamp.getTime() === (beforeDate instanceof Date && beforeDate.getTime()))) {
-							event.target.result.continue();
-							return;
-						}
+			});
+		},
 
-						result.unshift(hydrateMessage(value));
-						event.target.result.continue();
-					} else {
-						Promise.all(result).then(callback);
-					}
-				}
-				cursor.onerror = (event) => {
-					console.error(event);
-					callback([]);
+		updateMessageStatus: function(account, localId, status, callback) {
+			const tx = db.transaction(["messages"], "readwrite");
+			const store = tx.objectStore("messages");
+			promisifyRequest(store.index("localId").openCursor(IDBKeyRange.bound([account, localId], [account, localId, []]))).then((result) => {
+				if (result?.value && result.value.direction === enums.MessageDirection.MessageSent && result.value.status !== enums.MessageStatus.MessageDeliveredToDevice) {
+					const newStatus = { ...result.value, status: status };
+					result.update(newStatus);
+					hydrateMessage(newStatus).then(callback);
 				}
-			},
-
-			getMediaUri: function(hashAlgorithm, hash, callback) {
-				(async function() {
-					var niUrl;
-					if (hashAlgorithm == "sha-256") {
-						niUrl = mkNiUrl(hashAlgorithm, hash);
-					} else {
-						const tx = db.transaction(["keyvaluepairs"], "readonly");
-						const store = tx.objectStore("keyvaluepairs");
-						niUrl = await promisifyRequest(store.get(mkNiUrl(hashAlgorithm, hash)));
-						if (!niUrl) {
-							return null;
-						}
-					}
+			});
+		},
 
-					const response = await cache.match(niUrl);
-					if (response) {
-						// NOTE: the application needs to call URL.revokeObjectURL on this when done
-					  return URL.createObjectURL(await response.blob());
+		getMessages: function(account, chatId, beforeId, beforeTime, callback) {
+			const beforeDate = beforeTime ? new Date(beforeTime) : [];
+			const tx = db.transaction(["messages"], "readonly");
+			const store = tx.objectStore("messages");
+			const cursor = store.index("chats").openCursor(
+				IDBKeyRange.bound([account, chatId], [account, chatId, beforeDate]),
+				"prev"
+			);
+			const result = [];
+			cursor.onsuccess = (event) => {
+				if (event.target.result && result.length < 50) {
+					const value = event.target.result.value;
+					if (value.serverId === beforeId || (value.timestamp && value.timestamp.getTime() === (beforeDate instanceof Date && beforeDate.getTime()))) {
+						event.target.result.continue();
+						return;
 					}
 
-					return null;
-				})().then(callback);
-			},
-
-			storeMedia: function(mime, buffer, callback) {
-				(async function() {
-					const sha256 = await crypto.subtle.digest("SHA-256", buffer);
-					const sha512 = await crypto.subtle.digest("SHA-512", buffer);
-					const sha1 = await crypto.subtle.digest("SHA-1", buffer);
-					const sha256NiUrl = mkNiUrl("sha-256", sha256);
-					await cache.put(sha256NiUrl, new Response(buffer, { headers: { "Content-Type": mime } }));
-
-					const tx = db.transaction(["keyvaluepairs"], "readwrite");
-					const store = tx.objectStore("keyvaluepairs");
-					await promisifyRequest(store.put(sha256NiUrl, mkNiUrl("sha-1", sha1)));
-					await promisifyRequest(store.put(sha256NiUrl, mkNiUrl("sha-512", sha512)));
-				})().then(callback);
-			},
-
-			storeCaps: function(caps) {
-				const tx = db.transaction(["keyvaluepairs"], "readwrite");
-				const store = tx.objectStore("keyvaluepairs");
-				store.put(caps, "caps:" + caps.ver()).onerror = console.error;
-			},
+					result.unshift(hydrateMessage(value));
+					event.target.result.continue();
+				} else {
+					Promise.all(result).then(callback);
+				}
+			}
+			cursor.onerror = (event) => {
+				console.error(event);
+				callback([]);
+			}
+		},
 
-			getCaps: function(ver, callback) {
-				(async function() {
+		getMediaUri: function(hashAlgorithm, hash, callback) {
+			(async function() {
+				var niUrl;
+				if (hashAlgorithm == "sha-256") {
+					niUrl = mkNiUrl(hashAlgorithm, hash);
+				} else {
 					const tx = db.transaction(["keyvaluepairs"], "readonly");
 					const store = tx.objectStore("keyvaluepairs");
-					const raw = await promisifyRequest(store.get("caps:" + ver));
-					if (raw) {
-						return (new snikket.Caps(raw.node, raw.identities.map((identity) => new snikket.Identity(identity.category, identity.type, identity.name)), raw.features));
+					niUrl = await promisifyRequest(store.get(mkNiUrl(hashAlgorithm, hash)));
+					if (!niUrl) {
+						return null;
 					}
+				}
 
-					return null;
-				})().then(callback);
-			},
-
-			storeLogin: function(login, clientId, displayName, token) {
-				const tx = db.transaction(["keyvaluepairs"], "readwrite");
-				const store = tx.objectStore("keyvaluepairs");
-				store.put(clientId, "login:clientId:" + login).onerror = console.error;
-				store.put(displayName, "fn:" + login).onerror = console.error;
-				if (token != null) {
-					store.put(token, "login:token:" + login).onerror = console.error;
-					store.put(0, "login:fastCount:" + login).onerror = console.error;
+				const response = await cache.match(niUrl);
+				if (response) {
+					// NOTE: the application needs to call URL.revokeObjectURL on this when done
+				  return URL.createObjectURL(await response.blob());
 				}
-			},
 
-			storeStreamManagement: function(account, id, outbound, inbound, outbound_q) {
+				return null;
+			})().then(callback);
+		},
+
+		storeMedia: function(mime, buffer, callback) {
+			(async function() {
+				const sha256 = await crypto.subtle.digest("SHA-256", buffer);
+				const sha512 = await crypto.subtle.digest("SHA-512", buffer);
+				const sha1 = await crypto.subtle.digest("SHA-1", buffer);
+				const sha256NiUrl = mkNiUrl("sha-256", sha256);
+				await cache.put(sha256NiUrl, new Response(buffer, { headers: { "Content-Type": mime } }));
+
 				const tx = db.transaction(["keyvaluepairs"], "readwrite");
 				const store = tx.objectStore("keyvaluepairs");
-				store.put({ id: id, outbound: outbound, inbound: inbound, outbound_q }, "sm:" + account).onerror = console.error;
-			},
-
-			getStreamManagement: function(account, callback) {
+				await promisifyRequest(store.put(sha256NiUrl, mkNiUrl("sha-1", sha1)));
+				await promisifyRequest(store.put(sha256NiUrl, mkNiUrl("sha-512", sha512)));
+			})().then(callback);
+		},
+
+		storeCaps: function(caps) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			store.put(caps, "caps:" + caps.ver()).onerror = console.error;
+		},
+
+		getCaps: function(ver, callback) {
+			(async function() {
 				const tx = db.transaction(["keyvaluepairs"], "readonly");
 				const store = tx.objectStore("keyvaluepairs");
-				promisifyRequest(store.get("sm:" + account)).then(
-					(v) => {
-						callback(v?.id, v?.outbound, v?.inbound, v?.outbound_q || []);
-					},
-					(e) => {
-						console.error(e);
-						callback(null, -1, -1, []);
-					}
-				);
-			},
+				const raw = await promisifyRequest(store.get("caps:" + ver));
+				if (raw) {
+					return (new snikket.Caps(raw.node, raw.identities.map((identity) => new snikket.Identity(identity.category, identity.type, identity.name)), raw.features));
+				}
 
-			getLogin: function(login, callback) {
-				const tx = db.transaction(["keyvaluepairs"], "readwrite");
-				const store = tx.objectStore("keyvaluepairs");
-				Promise.all([
-					promisifyRequest(store.get("login:clientId:" + login)),
-					promisifyRequest(store.get("login:token:" + login)),
-					promisifyRequest(store.get("login:fastCount:" + login)),
-					promisifyRequest(store.get("fn:" + login)),
-				]).then((result) => {
-					if (result[1]) {
-						store.put((result[2] || 0) + 1, "login:fastCount:" + login).onerror = console.error;
-					}
-					callback(result[0], result[1], result[2] || 0, result[3]);
-				}).catch((e) => {
+				return null;
+			})().then(callback);
+		},
+
+		storeLogin: function(login, clientId, displayName, token) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			store.put(clientId, "login:clientId:" + login).onerror = console.error;
+			store.put(displayName, "fn:" + login).onerror = console.error;
+			if (token != null) {
+				store.put(token, "login:token:" + login).onerror = console.error;
+				store.put(0, "login:fastCount:" + login).onerror = console.error;
+			}
+		},
+
+		storeStreamManagement: function(account, id, outbound, inbound, outbound_q) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			store.put({ id: id, outbound: outbound, inbound: inbound, outbound_q }, "sm:" + account).onerror = console.error;
+		},
+
+		getStreamManagement: function(account, callback) {
+			const tx = db.transaction(["keyvaluepairs"], "readonly");
+			const store = tx.objectStore("keyvaluepairs");
+			promisifyRequest(store.get("sm:" + account)).then(
+				(v) => {
+					callback(v?.id, v?.outbound, v?.inbound, v?.outbound_q || []);
+				},
+				(e) => {
 					console.error(e);
-					callback(null, null, 0, null);
-				});
-			},
+					callback(null, -1, -1, []);
+				}
+			);
+		},
+
+		getLogin: function(login, callback) {
+			const tx = db.transaction(["keyvaluepairs"], "readwrite");
+			const store = tx.objectStore("keyvaluepairs");
+			Promise.all([
+				promisifyRequest(store.get("login:clientId:" + login)),
+				promisifyRequest(store.get("login:token:" + login)),
+				promisifyRequest(store.get("login:fastCount:" + login)),
+				promisifyRequest(store.get("fn:" + login)),
+			]).then((result) => {
+				if (result[1]) {
+					store.put((result[2] || 0) + 1, "login:fastCount:" + login).onerror = console.error;
+				}
+				callback(result[0], result[1], result[2] || 0, result[3]);
+			}).catch((e) => {
+				console.error(e);
+				callback(null, null, 0, null);
+			});
+		},
 
-			storeService(account, serviceId, name, node, caps) {
-				this.storeCaps(caps);
+		storeService(account, serviceId, name, node, caps) {
+			this.storeCaps(caps);
 
-				const tx = db.transaction(["services"], "readwrite");
-				const store = tx.objectStore("services");
+			const tx = db.transaction(["services"], "readwrite");
+			const store = tx.objectStore("services");
 
-				store.put({
-					account: account,
-					serviceId: serviceId,
-					name: name,
-					node: node,
-					caps: caps.ver(),
-				});
-			},
-
-			findServicesWithFeature(account, feature, callback) {
-				const tx = db.transaction(["services"], "readonly");
-				const store = tx.objectStore("services");
-
-				// Almost full scan shouldn't be too expensive, how many services are we aware of?
-				const cursor = store.openCursor(IDBKeyRange.bound([account], [account, []]));
-				const result = [];
-				cursor.onsuccess = (event) => {
-					if (event.target.result) {
-						const value = event.target.result.value;
-						result.push(new Promise((resolve) => this.getCaps(value.caps, (caps) => resolve({ ...value, caps: caps }))));
-						event.target.result.continue();
-					} else {
-						Promise.all(result).then((items) => items.filter((item) => item.caps && item.caps.features.includes(feature))).then(callback);
-					}
-				}
-				cursor.onerror = (event) => {
-					console.error(event);
-					callback([]);
+			store.put({
+				account: account,
+				serviceId: serviceId,
+				name: name,
+				node: node,
+				caps: caps.ver(),
+			});
+		},
+
+		findServicesWithFeature(account, feature, callback) {
+			const tx = db.transaction(["services"], "readonly");
+			const store = tx.objectStore("services");
+
+			// Almost full scan shouldn't be too expensive, how many services are we aware of?
+			const cursor = store.openCursor(IDBKeyRange.bound([account], [account, []]));
+			const result = [];
+			cursor.onsuccess = (event) => {
+				if (event.target.result) {
+					const value = event.target.result.value;
+					result.push(new Promise((resolve) => this.getCaps(value.caps, (caps) => resolve({ ...value, caps: caps }))));
+					event.target.result.continue();
+				} else {
+					Promise.all(result).then((items) => items.filter((item) => item.caps && item.caps.features.includes(feature))).then(callback);
 				}
 			}
+			cursor.onerror = (event) => {
+				console.error(event);
+				callback([]);
+			}
 		}
 	}
 };
+
+export default browser;