git » sdk » commit 701f6b8

Fix most noemit, clean up headerfile

author Stephen Paul Weber
2024-03-11 21:27:45 UTC
committer Stephen Paul Weber
2024-03-11 21:27:45 UTC
parent 799f1709a71b458b0ae486e36b3a1c2af1c2a59c

Fix most noemit, clean up headerfile

exposed enums are enum abstract Int now, and we use dummy classes to
expose the values to JavaScript

HaxeCBridge.hx +53 -22
Makefile +0 -1
snikket/Chat.hx +97 -36
snikket/ChatMessage.hx +0 -4
snikket/Client.hx +16 -17
snikket/Message.hx +26 -8
snikket/jingle/Sessin.hx +0 -0
snikket/jingle/Stream.cpp.hx +0 -0
snikket/persistence/browser.js +14 -33

diff --git a/HaxeCBridge.hx b/HaxeCBridge.hx
index c1e02c8..6778009 100644
--- a/HaxeCBridge.hx
+++ b/HaxeCBridge.hx
@@ -182,7 +182,7 @@ class HaxeCBridge {
 				case FFun(fun):
 					var wrapper = {
 						name: field.name + "__fromC",
-						doc: null,
+						doc: field.doc,
 						meta: [{name: "HaxeCBridge.wrapper", params: [], pos: Context.currentPos()}],
 						access: field.access.filter(a -> a != AAbstract),
 						kind: null,
@@ -191,6 +191,7 @@ class HaxeCBridge {
 					};
 					var args = [];
 					var passArgs = [];
+					var outPtr = false;
 					for (arg in fun.args) {
 						switch arg.type {
 						case TFunction(taargs, aret):
@@ -214,23 +215,42 @@ class HaxeCBridge {
 					}
 					switch (fun.ret) {
 					case TPath(path) if (path.name == "Array"):
-						wrapper.ret = TPath({name: "HaxeArray", pack: [], params: path.params.map(t -> convertSecondaryTP(t))});
+						outPtr = true;
+						args.push({name: "outPtr", type: TPath({name: "RawPointer", pack: ["cpp"], params: [TPType(TPath({name: "HaxeArray", pack: [], params: path.params.map(t -> convertSecondaryTP(t))}))]})});
+						wrapper.ret = TPath({name: "SizeT", pack: ["cpp"]});
 					default:
 					}
 					if (args.length > fun.args.length || fun.ret != wrapper.ret || field.access.contains(AAbstract)) {
-						wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro return $i{field.name}($a{passArgs}), args: args});
+						if (outPtr) {
+							wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro { final out = $i{field.name}($a{passArgs}); if (outPtr != null) { cpp.Pointer.fromRaw(outPtr).set_ref(out); } return out.length; }, args: args});
+						} else {
+							wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro return $i{field.name}($a{passArgs}), args: args});
+						}
 						fields.push(wrapper);
 						field.meta.push({name: "HaxeCBridge.noemit", pos: Context.currentPos()});
 					}
 			case FVar(t, e):
-					fields.push({
-						name: field.name + "__fromC",
-						doc: null,
-						meta: [{name: "HaxeCBridge.wrapper", params: [], pos: Context.currentPos()}],
-						access: field.access,
-						pos: Context.currentPos(),
-						kind: FFun({ret: t, params: [], args: [], expr: macro return $i{field.name}})
-					});
+					switch (t) {
+					case TPath(path) if (path.name == "Array"):
+						final ptrT = TPath({name: "RawPointer", pack: ["cpp"], params: [TPType(TPath({name: "HaxeArray", pack: [], params: path.params.map(t -> convertSecondaryTP(t))}))]});
+						fields.push({
+							name: field.name + "__fromC",
+							doc: null,
+							meta: [{name: "HaxeCBridge.wrapper", params: [], pos: Context.currentPos()}],
+							access: field.access,
+							pos: Context.currentPos(),
+							kind: FFun({ret: TPath({name: "SizeT", pack: ["cpp"]}), params: [], args: [{name: "outPtr", type: ptrT}], expr: macro { if (outPtr != null) { cpp.Pointer.fromRaw(outPtr).set_ref($i{field.name}); } return $i{field.name}.length; } })
+						});
+					default:
+						fields.push({
+							name: field.name + "__fromC",
+							doc: null,
+							meta: [{name: "HaxeCBridge.wrapper", params: [], pos: Context.currentPos()}],
+							access: field.access,
+							pos: Context.currentPos(),
+							kind: FFun({ret: t, params: [], args: [], expr: macro return $i{field.name}})
+						});
+					}
 				default:
 				}
 			}
@@ -253,7 +273,7 @@ class HaxeCBridge {
 		case TInst(_.get().name => "Array", tps):
 			[
 				TPath({name: "HaxeArray", pack: [], params: tps.map((tp) -> convertSecondaryTP(TPType(Context.toComplexType(tp))))}),
-				TPath({name: "Int", pack: []})
+				TPath({name: "SizeT", pack: ["cpp"]})
 			];
 		case TInst(_.get().name => "String", _):
 			[TPath({name: "ConstCharStar", pack: ["cpp"], params: []})];
@@ -454,7 +474,7 @@ class HaxeCBridge {
 				');
 			} else '')
 
-			+ 'typedef void (* SnikketPanicCallback) (const char* exceptionInfo);\n'
+			+ 'typedef void (*snikket_panic_callback) (const char *info);\n'
 			+ (if (ctx.supportTypeDeclarations.length > 0) ctx.supportTypeDeclarations.map(d -> CPrinter.printDeclaration(d, true)).join(';\n') + ';\n\n'; else '')
 			+ (if (ctx.typeDeclarations.length > 0) ctx.typeDeclarations.map(d -> CPrinter.printDeclaration(d, true)).join(';\n') + ';\n'; else '')
 
@@ -472,7 +492,7 @@ class HaxeCBridge {
 				 * @param panicCallback a callback to execute if the SDK panics. The SDK will continue processing events after a panic and you may want to stop it after receiving this callback. Use `NULL` for no callback
 				 * @returns `NULL` if the thread initializes successfully or a null-terminated C string if an error occurs during initialization
 				 */
-				$prefix const char *${namespace}_setup(SnikketPanicCallback panicCallback);
+				$prefix const char *${namespace}_setup(snikket_panic_callback panic_callback);
 
 				/**
 				 * Stops the SDK, blocking until the main thread has completed. Once ended, it cannot be restarted (this is because static variable state will be retained from the last run).
@@ -483,9 +503,9 @@ class HaxeCBridge {
 				 * 
 				 * Thread-safety: Can be called safely called on any thread.
 				 *
-				 * @param waitOnScheduledEvents If `true`, this function will wait for all events scheduled to execute in the future on the SDK thread to complete. If `false`, immediate pending events will be finished and the SDK stopped without executing events scheduled in the future
+				 * @param wait If `true`, this function will wait for all events scheduled to execute in the future on the SDK thread to complete. If `false`, immediate pending events will be finished and the SDK stopped without executing events scheduled in the future
 				 */
-				$prefix void ${namespace}_stop(bool waitOnScheduledEvents);
+				$prefix void ${namespace}_stop(bool wait);
 
 		')
 		+ indent(1, ctx.supportFunctionDeclarations.map(fn -> CPrinter.printDeclaration(fn, true, prefix)).join(';\n\n') + ';\n\n')
@@ -578,7 +598,7 @@ class HaxeCBridge {
 				std::atomic<bool> staticsInitialized = { false };
 
 				struct HaxeThreadData {
-					SnikketPanicCallback haxeExceptionCallback;
+					snikket_panic_callback haxeExceptionCallback;
 					const char* initExceptionInfo;
 				};
 
@@ -632,7 +652,7 @@ class HaxeCBridge {
 				threadData->initExceptionInfo = nullptr;
 
 				// copy out callback
-				SnikketPanicCallback haxeExceptionCallback = threadData->haxeExceptionCallback;
+				snikket_panic_callback haxeExceptionCallback = threadData->haxeExceptionCallback;
 
 				bool firstRun = !HaxeCBridgeInternal::staticsInitialized;
 
@@ -663,7 +683,7 @@ class HaxeCBridge {
 			}
 			
 			HAXE_C_BRIDGE_LINKAGE
-			const char* ${namespace}_setup(SnikketPanicCallback unhandledExceptionCallback) {
+			const char* ${namespace}_setup(snikket_panic_callback unhandledExceptionCallback) {
 				HaxeCBridgeInternal::HaxeThreadData threadData;
 				threadData.haxeExceptionCallback = unhandledExceptionCallback == nullptr ? HaxeCBridgeInternal::defaultExceptionHandler : unhandledExceptionCallback;
 				threadData.initExceptionInfo = nullptr;
@@ -1050,7 +1070,7 @@ class CPrinter {
 		var name = signature.name;
 		var args = signature.args;
 		var ret = signature.ret;
-		return '${printType(ret)} $name(${args.map(arg -> '${printType(arg.type, arg.name)}').join(', ')})'.replace(" * ", " *").replace("(* ", "(*").replace("* ", " *");
+		return '${printType(ret)} $name(${args.map(arg -> '${printType(arg.type, arg.name)}').join(', ')})'.replace(" * ", " *").replace("(* ", "(*").replace("* ", " *").replace("** ", " **");
 	}
 
 	public static function printDoc(doc: String) {
@@ -1473,7 +1493,8 @@ class CConverterContext {
 	}
 
 	function getEnumCType(type: Type, allowNonTrivial: Bool, pos: Position): CType {
-		var ident = safeIdent(declarationPrefix + '_' + typeDeclarationIdent(type, false));
+		//var ident = safeIdent(declarationPrefix + '_' + typeDeclarationIdent(type, false));
+		final ident = hx.strings.Strings.toLowerUnderscore(safeIdent(typeDeclarationIdent(type, false)));
 
 		// `enum ident` is considered non-trivial
 		if (!allowNonTrivial) {
@@ -1659,7 +1680,7 @@ class CConverterContext {
 		"auto", "double", "int", "struct", "break", "else", "long", "switch", "case", "enum", "register", "typedef", "char", "extern", "return", "union", "const", "float", "short", "unsigned", "continue", "for", "signed", "void", "default", "goto", "sizeof", "volatile", "do", "if", "static", "while",
 		"size_t", "int64_t", "uint64_t",
 		// HaxeCBridge types
-		"HaxeObject", "SnikketPanicCallback",
+		"HaxeObject", "snikket_panic_callback",
 		// hxcpp
 		"Int", "String", "Float", "Dynamic", "Bool",
 	];
@@ -1748,6 +1769,16 @@ abstract HaxeObject<T>(cpp.RawPointer<cpp.Void>) from cpp.RawPointer<cpp.Void> t
 }
 
 abstract HaxeArray<T>(cpp.RawPointer<cpp.RawPointer<cpp.Void>>) from cpp.RawPointer<cpp.RawPointer<cpp.Void>> to cpp.RawPointer<cpp.RawPointer<cpp.Void>> {
+	@:from
+	public static inline function fromArrayString(x: Array<String>): HaxeArray<cpp.ConstCharStar> {
+		final arr: Array<cpp.SizeT> = cpp.NativeArray.create(x.length);
+		for (i => el in x) {
+			final ptr = cpp.ConstCharStar.fromString(el);
+			arr[i] = untyped untyped __cpp__('reinterpret_cast<size_t>({0})', ptr);
+		}
+		return cast HaxeCBridge.retainHaxeArray(arr);
+	}
+
 	@:from
 	public static inline function fromArrayT<T>(x: Array<T>): HaxeArray<HaxeObject<T>> {
 		return HaxeCBridge.retainHaxeArray(x);
diff --git a/Makefile b/Makefile
index aa8c747..65a7096 100644
--- a/Makefile
+++ b/Makefile
@@ -13,6 +13,5 @@ run-nodejs: test.node.js
 browser.js:
 	haxe browser.hxml
 	echo "var exports = {};" > browser.js
-	sed -e 's/hxEnums\["snikket.EventResult"\] = {/hxEnums["snikket.EventResult"] = $$hx_exports.snikket.EventResult = {/' < browser.haxe.js | sed -e 's/hxEnums\["snikket.MessageDirection"\] = {/hxEnums["snikket.MessageDirection"] = $$hx_exports.snikket.MessageDirection = {/' | sed -e 's/hxEnums\["snikket.UiState"\] = {/hxEnums["snikket.UiState"] = $$hx_exports.snikket.UiState = {/' | sed -e 's/hxEnums\["snikket.MessageStatus"\] = {/hxEnums["snikket.MessageStatus"] = $$hx_exports.snikket.MessageStatus = {/' >> browser.js
 	cat snikket/persistence/*.js >> browser.js
 	echo "export const { snikket } = exports;" >> browser.js
diff --git a/snikket/Chat.hx b/snikket/Chat.hx
index 803a554..fbe71c1 100644
--- a/snikket/Chat.hx
+++ b/snikket/Chat.hx
@@ -17,12 +17,21 @@ using Lambda;
 import HaxeCBridge;
 #end
 
-enum UiState {
-	Pinned;
-	Open; // or Unspecified
-	Closed; // Archived
+enum abstract UiState(Int) {
+	var Pinned;
+	var Open; // or Unspecified
+	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())
 #end
@@ -35,16 +44,17 @@ abstract class Chat {
 	private var presence:Map<String, Presence> = [];
 	private var trusted:Bool = false;
 	public var chatId(default, null):String;
-	public var jingleSessions: Map<String, snikket.jingle.Session> = [];
+	@:allow(snikket)
+	private var jingleSessions: Map<String, snikket.jingle.Session> = [];
 	private var displayName:String;
-	@HaxeCBridge.noemit
 	public var uiState = Open;
-	public var extensions: Stanza;
+	@:allow(snikket)
+	private var extensions: Stanza;
 	private var _unreadCount = 0;
 	private var lastMessage: Null<ChatMessage>;
 
 	@:allow(snikket)
-	private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState:Dynamic = Open, extensions: Null<Stanza> = null) {
+	private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, extensions: Null<Stanza> = null) {
 		this.client = client;
 		this.stream = stream;
 		this.persistence = persistence;
@@ -54,19 +64,20 @@ abstract class Chat {
 		this.displayName = chatId;
 	}
 
-	abstract public function prepareIncomingMessage(message:ChatMessage, stanza:Stanza):ChatMessage;
+	@:allow(snikket)
+	abstract private function prepareIncomingMessage(message:ChatMessage, stanza:Stanza):ChatMessage;
 
 	abstract public function correctMessage(localId:String, message:ChatMessage):Void;
 
 	abstract public function sendMessage(message:ChatMessage):Void;
 
+	abstract public function removeReaction(m:ChatMessage, reaction:String):Void;
+
 	abstract public function getMessages(beforeId:Null<String>, beforeTime:Null<String>, handler:(Array<ChatMessage>)->Void):Void;
 
-	@HaxeCBridge.noemit
 	abstract public function getParticipants():Array<String>;
 
-	@HaxeCBridge.noemit
-	abstract public function getParticipantDetails(participantId:String, callback:({photoUri:String, displayName:String})->Void):Void;
+	abstract public function getParticipantDetails(participantId:String, callback:(String, String)->Void):Void;
 
 	abstract public function bookmark():Void;
 
@@ -111,7 +122,8 @@ abstract class Chat {
 		return _unreadCount;
 	}
 
-	public function setUnreadCount(count:Int) {
+	@:allow(snikket)
+	private function setUnreadCount(count:Int) {
 		_unreadCount = count;
 	}
 
@@ -119,7 +131,8 @@ abstract class Chat {
 		return lastMessage?.text ?? "";
 	}
 
-	public function setLastMessage(message:Null<ChatMessage>) {
+	@:allow(snikket)
+	private function setLastMessage(message:Null<ChatMessage>) {
 		lastMessage = message;
 	}
 
@@ -131,11 +144,13 @@ abstract class Chat {
 		return this.displayName;
 	}
 
-	public function setPresence(resource:String, presence:Presence) {
+	@:allow(snikket)
+	private function setPresence(resource:String, presence:Presence) {
 		this.presence.set(resource, presence);
 	}
 
-	public function setCaps(resource:String, caps:Caps) {
+	@:allow(snikket)
+	private function setCaps(resource:String, caps:Caps) {
 		final presence = presence.get(resource);
 		if (presence != null) {
 			presence.caps = caps;
@@ -145,11 +160,13 @@ abstract class Chat {
 		}
 	}
 
-	public function removePresence(resource:String) {
+	@:allow(snikket)
+	private function removePresence(resource:String) {
 		presence.remove(resource);
 	}
 
-	public function getCaps():KeyValueIterator<String, Caps> {
+	@:allow(snikket)
+	private function getCaps():KeyValueIterator<String, Caps> {
 		final iter = presence.keyValueIterator();
 		return {
 			hasNext: iter.hasNext,
@@ -160,7 +177,8 @@ abstract class Chat {
 		};
 	}
 
-	public function getResourceCaps(resource:String):Caps {
+	@:allow(snikket)
+	private function getResourceCaps(resource:String):Caps {
 		return presence[resource]?.caps ?? new Caps("", [], []);
 	}
 
@@ -177,7 +195,8 @@ abstract class Chat {
 		return this.trusted;
 	}
 
-	public function livePresence() {
+	@:allow(snikket)
+	private function livePresence() {
 		return false;
 	}
 
@@ -239,8 +258,7 @@ abstract class Chat {
 		return null;
 	}
 
-	@HaxeCBridge.noemit
-	public function videoTracks() {
+	public function videoTracks(): Array<MediaStreamTrack> {
 		return jingleSessions.flatMap((session) -> session.videoTracks());
 	}
 
@@ -275,9 +293,9 @@ class DirectChat extends Chat {
 	}
 
 	@HaxeCBridge.noemit // on superclass as abstract
-	public function getParticipantDetails(participantId:String, callback:({photoUri:String, displayName:String})->Void) {
+	public function getParticipantDetails(participantId:String, callback:(String, String)->Void) {
 		final chat = client.getDirectChat(participantId);
-		chat.getPhoto((photoUri) -> callback({ photoUri: photoUri, displayName: chat.getDisplayName() }));
+		chat.getPhoto((photoUri) -> callback(photoUri, chat.getDisplayName()));
 	}
 
 	@HaxeCBridge.noemit // on superclass as abstract
@@ -309,7 +327,8 @@ class DirectChat extends Chat {
 		});
 	}
 
-	public function prepareIncomingMessage(message:ChatMessage, stanza:Stanza) {
+	@:allow(snikket)
+	private function prepareIncomingMessage(message:ChatMessage, stanza:Stanza) {
 		message.syncPoint = true; // TODO: if client is done initial MAM. right now it always is
 		return message;
 	}
@@ -373,6 +392,7 @@ class DirectChat extends Chat {
 		}
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function removeReaction(m:ChatMessage, reaction:String) {
 		// NOTE: doing it this way means no fallback behaviour
 		final reactions = [];
@@ -390,10 +410,12 @@ class DirectChat extends Chat {
 		});
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function lastMessageId() {
 		return lastMessage?.localId ?? lastMessage?.serverId;
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function markReadUpTo(message: ChatMessage) {
 		if (readUpTo() == message.localId || readUpTo() == message.serverId) return;
 		final upTo = message.localId ?? message.serverId;
@@ -420,6 +442,7 @@ class DirectChat extends Chat {
 		client.trigger("chats/update", [this]);
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function bookmark() {
 		stream.sendIq(
 			new Stanza("iq", { type: "set" })
@@ -434,6 +457,7 @@ class DirectChat extends Chat {
 		);
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function close() {
 		// Should this remove from roster?
 		uiState = Closed;
@@ -443,11 +467,16 @@ class DirectChat extends Chat {
 }
 
 @:expose
+#if cpp
+@:build(HaxeCBridge.expose())
+#end
 class Channel extends Chat {
-	public var disco: Caps = new Caps("", [], ["http://jabber.org/protocol/muc"]);
+	@:allow(snikket)
+	private var disco: Caps = new Caps("", [], ["http://jabber.org/protocol/muc"]);
 	private var inSync = true;
 
-	public function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, extensions = null, ?disco: Caps) {
+	@:allow(snikket)
+	private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, extensions = null, ?disco: Caps) {
 		super(client, stream, persistence, chatId, uiState, extensions);
 		if (disco != null) this.disco = disco;
 	}
@@ -572,7 +601,8 @@ class Channel extends Chat {
 		return lastMessage.sender.resource + ": " + super.preview();
 	}
 
-	override public function livePresence() {
+	@:allow(snikket)
+	override private function livePresence() {
 		for (nick => p in presence) {
 			for (status in p?.mucUser?.allTags("status") ?? []) {
 				if (status.attr.get("code") == "110") {
@@ -598,33 +628,32 @@ class Channel extends Chat {
 		return JID.parse(chatId).withResource(nickInUse());
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function getParticipants() {
 		final jid = JID.parse(chatId);
 		return { iterator: () -> presence.keys() }.map((resource) -> new JID(jid.node, jid.domain, resource).asString());
 	}
 
-	public function getParticipantDetails(participantId:String, callback:({photoUri:String, displayName:String})->Void) {
+	@HaxeCBridge.noemit // on superclass as abstract
+	public function getParticipantDetails(participantId:String, callback:(String, String)->Void) {
 		if (participantId == getFullJid().asString()) {
 			client.getDirectChat(client.accountId(), false).getPhoto((photoUri) -> {
-				callback({ photoUri: photoUri, displayName: client.displayName() });
+				callback(photoUri, client.displayName());
 			});
 		} else {
 			final nick = JID.parse(participantId).resource;
 			final photoUri = Color.defaultPhoto(participantId, nick == null ? " " : nick.charAt(0));
-			callback({ photoUri: photoUri, displayName: nick });
+			callback(photoUri, nick);
 		}
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function getMessages(beforeId:Null<String>, beforeTime:Null<String>, handler:(Array<ChatMessage>)->Void):Void {
-		trace("1");
 		return;
 		persistence.getMessages(client.accountId(), chatId, beforeId, beforeTime, (messages) -> {
-		trace("2");
 			if (messages.length > 0) {
-		trace("3");
 				handler(messages);
 			} else {
-		trace("4");
 				var filter:MAMQueryParams = {};
 				if (beforeId != null) filter.page = { before: beforeId };
 				var sync = new MessageSync(this.client, this.stream, filter, chatId);
@@ -649,7 +678,8 @@ class Channel extends Chat {
 		});
 	}
 
-	public function prepareIncomingMessage(message:ChatMessage, stanza:Stanza) {
+	@:allow(snikket)
+	private function prepareIncomingMessage(message:ChatMessage, stanza:Stanza) {
 		message.syncPoint = inSync;
 		message.sender = JID.parse(stanza.attr.get("from")); // MUC always needs full JIDs
 		if (message.senderId() == getFullJid().asString()) {
@@ -671,6 +701,7 @@ class Channel extends Chat {
 		return message;
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function correctMessage(localId:String, message:ChatMessage) {
 		final toSend = message.clone();
 		message = prepareOutgoingMessage(message);
@@ -687,6 +718,7 @@ class Channel extends Chat {
 		});
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function sendMessage(message:ChatMessage):Void {
 		client.chatActivity(this);
 		message = prepareOutgoingMessage(message);
@@ -714,6 +746,7 @@ class Channel extends Chat {
 		}
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function removeReaction(m:ChatMessage, reaction:String) {
 		// NOTE: doing it this way means no fallback behaviour
 		final reactions = [];
@@ -729,10 +762,12 @@ class Channel extends Chat {
 		});
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function lastMessageId() {
 		return lastMessage?.serverId;
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function markReadUpTo(message: ChatMessage) {
 		if (readUpTo() == message.serverId) return;
 		final upTo = message.serverId;
@@ -757,6 +792,7 @@ class Channel extends Chat {
 		client.trigger("chats/update", [this]);
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function bookmark() {
 		stream.sendIq(
 			new Stanza("iq", { type: "set" })
@@ -807,6 +843,7 @@ class Channel extends Chat {
 		);
 	}
 
+	@HaxeCBridge.noemit // on superclass as abstract
 	public function close() {
 		uiState = Closed;
 		persistence.storeChat(client.accountId(), this);
@@ -816,6 +853,30 @@ class Channel extends Chat {
 	}
 }
 
+@:expose
+#if cpp
+@:build(HaxeCBridge.expose())
+#end
+class AvailableChat {
+	public final chatId: String;
+	public final displayName: Null<String>;
+	public final note: String;
+	@:allow(snikket)
+	private final caps: Caps;
+
+	public function isChannel() {
+		return caps.isChannel(chatId);
+	}
+
+	@:allow(snikket)
+	private function new(chatId: String, displayName: Null<String>, note: String, caps: Caps) {
+		this.chatId = chatId;
+		this.displayName = displayName;
+		this.note = note;
+		this.caps = caps;
+	}
+}
+
 @:expose
 class SerializedChat {
 	public final chatId:String;
diff --git a/snikket/ChatMessage.hx b/snikket/ChatMessage.hx
index fbea4dd..ac558cf 100644
--- a/snikket/ChatMessage.hx
+++ b/snikket/ChatMessage.hx
@@ -61,7 +61,6 @@ class ChatMessage {
 	public var replyToMessage: Null<ChatMessage> = null;
 	public var threadId: Null<String> = null;
 
-	@HaxeCBridge.noemit
 	public var attachments: Array<ChatAttachment> = [];
 	public var reactions: Map<String, Array<String>> = [];
 
@@ -69,11 +68,8 @@ class ChatMessage {
 	public var lang: Null<String> = null;
 
 	public var isGroupchat: Bool = false; // Only really useful for distinguishing whispers
-	@HaxeCBridge.noemit
 	public var direction: MessageDirection = MessageReceived;
-	@HaxeCBridge.noemit
 	public var status: MessageStatus = MessagePending;
-	@HaxeCBridge.noemit
 	public var versions: Array<ChatMessage> = [];
 	@:allow(snikket)
 	private var payloads: Array<Stanza> = [];
diff --git a/snikket/Client.hx b/snikket/Client.hx
index a92e579..09a9ec3 100644
--- a/snikket/Client.hx
+++ b/snikket/Client.hx
@@ -379,7 +379,7 @@ class Client extends EventEmitter {
 	/**
 		Get the account ID for this Client
 
-		@return account id
+		@returns account id
 	**/
 	public function accountId() {
 		return jid.asBare().asString();
@@ -600,19 +600,19 @@ class Client extends EventEmitter {
 	}
 	#end
 
-	/* Return array of chats, sorted by last activity */
+	/**
+		@returns array of chats, sorted by last activity
+	 */
 	public function getChats():Array<Chat> {
 		return chats.filter((chat) -> chat.uiState != Closed);
 	}
 
-	// We can ask for caps here because presumably they looked this up
-	// via findAvailableChats
-	public function startChat(chatId:String, displayName:Null<String>, caps:Caps):Chat {
-		final existingChat = getChat(chatId);
+	public function startChat(availableChat: AvailableChat):Chat {
+		final existingChat = getChat(availableChat.chatId);
 		if (existingChat != null) {
 			final channel = Std.downcast(existingChat, Channel);
-			if (channel == null && caps.isChannel(chatId)) {
-				chats = chats.filter((chat) -> chat.chatId == chatId);
+			if (channel == null && availableChat.isChannel()) {
+				chats = chats.filter((chat) -> chat.chatId != availableChat.chatId);
 			} else {
 				if (existingChat.uiState == Closed) existingChat.uiState = Open;
 				channel?.selfPing();
@@ -621,15 +621,15 @@ class Client extends EventEmitter {
 			}
 		}
 
-		final chat = if (caps.isChannel(chatId)) {
-			final channel = new Channel(this, this.stream, this.persistence, chatId, Open, null, caps);
+		final chat = if (availableChat.isChannel()) {
+			final channel = new Channel(this, this.stream, this.persistence, availableChat.chatId, Open, null, availableChat.caps);
 			chats.unshift(channel);
 			channel.selfPing(false);
 			channel;
 		} else {
-			getDirectChat(chatId, false);
+			getDirectChat(availableChat.chatId, false);
 		}
-		if (displayName != null) chat.setDisplayName(displayName);
+		if (availableChat.displayName != null) chat.setDisplayName(availableChat.displayName);
 		persistence.storeChat(accountId(), chat);
 		this.trigger("chats/update", [chat]);
 		return chat;
@@ -652,8 +652,7 @@ class Client extends EventEmitter {
 		return chat;
 	}
 
-	@HaxeCBridge.noemit
-	public function findAvailableChats(q:String, callback:(String, Array<{ chatId: String, fn: String, note: String, caps: Caps }>) -> Void) {
+	public function findAvailableChats(q:String, callback:(String, Array<AvailableChat>) -> Void) {
 		var results = [];
 		final query = StringTools.trim(q);
 		final jid = JID.parse(query);
@@ -664,14 +663,14 @@ class Client extends EventEmitter {
 				if (resultCaps == null) {
 					final err = discoGet.responseStanza?.getChild("error")?.getChild(null, "urn:ietf:params:xml:ns:xmpp-stanzas");
 					if (err == null || err?.name == "service-unavailable" || err?.name == "feature-not-implemented") {
-						results.push({ chatId: jid.asString(), fn: query, note: jid.asString(), caps: new Caps("", [], []) });
+						results.push(new AvailableChat(jid.asString(), query, jid.asString(), new Caps("", [], [])));
 					}
 				} else {
 					persistence.storeCaps(resultCaps);
 					final identity = resultCaps.identities[0];
-					final fn = identity?.name ?? query;
+					final displayName = identity?.name ?? query;
 					final note = jid.asString() + (identity == null ? "" : " (" + identity.type + ")");
-					results.push({ chatId: jid.asString(), fn: fn, note: note, caps: resultCaps });
+					results.push(new AvailableChat(jid.asString(), displayName, note, resultCaps));
 				}
 				callback(q, results);
 			});
diff --git a/snikket/Message.hx b/snikket/Message.hx
index 863b2d2..bee5488 100644
--- a/snikket/Message.hx
+++ b/snikket/Message.hx
@@ -2,17 +2,35 @@ package snikket;
 
 using Lambda;
 
-enum MessageDirection {
-	MessageReceived;
-	MessageSent;
+enum abstract MessageDirection(Int) {
+	var MessageReceived;
+	var MessageSent;
 }
 
-enum MessageStatus {
-	MessagePending; // Message is waiting in client for sending
-	MessageDeliveredToServer; // Server acknowledged receipt of the message
-	MessageDeliveredToDevice; //The message has been delivered to at least one client device
-	MessageFailedToSend; // There was an error sending this message
+#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
+	var MessageDeliveredToDevice; //The message has been delivered to at least one client device
+	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);
diff --git a/snikket/jingle/Sessin.hx b/snikket/jingle/Sessin.hx
new file mode 100644
index 0000000..e69de29
diff --git a/snikket/jingle/Stream.cpp.hx b/snikket/jingle/Stream.cpp.hx
new file mode 100644
index 0000000..e69de29
diff --git a/snikket/persistence/browser.js b/snikket/persistence/browser.js
index 6b8ba16..e800db3 100644
--- a/snikket/persistence/browser.js
+++ b/snikket/persistence/browser.js
@@ -1,7 +1,7 @@
 // This example persistence driver is written in JavaScript
 // so that SDK users can easily see how to write their own
 
-exports.xmpp.persistence = {
+exports.snikket.persistence = {
 	browser: (dbname) => {
 		var db = null;
 		function openDb(version) {
@@ -62,17 +62,17 @@ exports.xmpp.persistence = {
 			const store = tx.objectStore("messages");
 			let replyToMessage = value.replyToMessage && await hydrateMessage((await promisifyRequest(store.openCursor(IDBKeyRange.only(value.replyToMessage))))?.value);
 
-			const message = new xmpp.ChatMessage();
+			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 && xmpp.JID.parse(value.to);
-			message.from = value.from && xmpp.JID.parse(value.from);
-			message.sender = value.sender && xmpp.JID.parse(value.sender);
-			message.recipients = value.recipients.map((r) => xmpp.JID.parse(r));
-			message.replyTo = value.replyTo.map((r) => xmpp.JID.parse(r));
+			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;
@@ -80,25 +80,8 @@ exports.xmpp.persistence = {
 			message.text = value.text;
 			message.lang = value.lang;
 			message.isGroupchat = value.isGroupchat || value.groupchat;
-			message.direction = value.direction == "MessageReceived" ? xmpp.MessageDirection.MessageReceived : xmpp.MessageDirection.MessageSent;
-			switch (value.status) {
-				case "MessagePending":
-					message.status = xmpp.MessageStatus.MessagePending;
-					break;
-				case "MessageDeliveredToServer":
-					message.status = xmpp.MessageStatus.MessageDeliveredToServer;
-					break;
-				case "MessageDeliveredToDevice":
-					message.status = xmpp.MessageStatus.MessageDeliveredToDevice;
-					break;
-				case "MessageFailedToSend":
-					message.status = xmpp.MessageStatus.MessageFailedToSend;
-					break;
-				default:
-					message.status = message.serverId ? xmpp.MessageStatus.MessageDeliveredToServer : xmpp.MessageStatus.MessagePending;
-			}
 			message.versions = await Promise.all((value.versions || []).map(hydrateMessage));
-			message.payloads = (value.payloads || []).map(xmpp.Stanza.parse);
+			message.payloads = (value.payloads || []).map(snikket.Stanza.parse);
 			return message;
 		}
 
@@ -118,8 +101,6 @@ exports.xmpp.persistence = {
 				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 || ""],
-				direction: message.direction.toString(),
-				status: message.status.toString(),
 				versions: message.versions.map((m) => serializeMessage(account, m)),
 				payloads: message.payloads.map((p) => p.toString()),
 			}
@@ -199,10 +180,10 @@ exports.xmpp.persistence = {
 					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?.toString(),
+					uiState: chat.uiState,
 					extensions: chat.extensions?.toString(),
 					disco: chat.disco,
-					class: chat instanceof xmpp.DirectChat ? "DirectChat" : (chat instanceof xmpp.Channel ? "Channel" : "Chat")
+					class: chat instanceof snikket.DirectChat ? "DirectChat" : (chat instanceof snikket.Channel ? "Channel" : "Chat")
 				});
 			},
 
@@ -212,12 +193,12 @@ exports.xmpp.persistence = {
 					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 xmpp.SerializedChat(
+					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 xmpp.Presence(p.caps && await new Promise((resolve) => this.getCaps(p.caps, resolve)), p.mucUser && xmpp.Stanza.parse(p.mucUser))]
+							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,
@@ -363,7 +344,7 @@ exports.xmpp.persistence = {
 				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.toString() };
+						const newStatus = { ...result.value, status: status };
 						result.update(newStatus);
 						hydrateMessage(newStatus).then(callback);
 					}
@@ -450,7 +431,7 @@ exports.xmpp.persistence = {
 					const store = tx.objectStore("keyvaluepairs");
 					const raw = await promisifyRequest(store.get("caps:" + ver));
 					if (raw) {
-						return (new xmpp.Caps(raw.node, raw.identities.map((identity) => new xmpp.Identity(identity.category, identity.type, identity.name)), raw.features));
+						return (new snikket.Caps(raw.node, raw.identities.map((identity) => new snikket.Identity(identity.category, identity.type, identity.name)), raw.features));
 					}
 
 					return null;