6

I played about with Kotlin's unsupported JavaScript backend in 1.0.x and am now trying to migrate my toy project to 1.1.x. It's the barest bones of a single-page web app interfacing with PouchDB. To add data to PouchDB you need JavaScript objects with specific properties _id and _rev. They also need to not have any other properties beginning with _ because they're reserved by PouchDB.

Now, if I create a class like this, I can send instances to PouchDB.

class PouchDoc(
        var _id: String
) {
    var _rev: String? = null
}

However, if I do anything to make the properties virtual -- have them override an interface, or make the class open and create a subclass which overrides them -- the _id field name becomes mangled to something like _id_mmz446$_0 and so PouchDB rejects the object. If I apply @JsName("_id") to the property, that only affects the generated getter and setter -- it still leaves the backing field with a mangled name.

Also, for any virtual properties whose names don't begin with _, PouchDB will accept the object but it only stores the backing fields with their mangled names, not the nicely-named properties.

For now I can work around things by making them not virtual, I think. But I was thinking of sharing interfaces between PouchDoc and non-PouchDoc classes in Kotlin, and it seems I can't do that.

Any idea how I could make this work, or does it need a Kotlin language change?

bashor
  • 8,073
  • 4
  • 34
  • 33
HughG
  • 1,108
  • 10
  • 14
  • How does it prevent sharing interfaces between PouchDoc and non-PouchDoc classes? – bashor May 02 '17 at 10:24
  • @bashor, if a PouchDoc and non-PouchDoc class implement the same interface (or, indeed, different interfaces) then the overriden properties of the interface become virtual in the class. So, they don't appear as simple properties in the JavaScript object, but like this: `Object.defineProperty(DocWrapper.prototype, '_id', { get: function () { return this.doc_mmz446$_0._id; } });` – HughG May 05 '17 at 14:38

2 Answers2

3

I think your problem should be covered by https://youtrack.jetbrains.com/issue/KT-8127

Also, I've created some other related issues: https://youtrack.jetbrains.com/issue/KT-17682 https://youtrack.jetbrains.com/issue/KT-17683

And right now You can use one of next solutions, IMO third is most lightweight.

interface PouchDoc1 {
    var id: String
    var _id: String
        get() = id
        set(v) { id = v}

    var rev: String?
    var _rev: String?
        get() = rev
        set(v) { rev = v}
}

class Impl1 : PouchDoc1 {
    override var id = "id0"
    override var rev: String? = "rev0"
}

interface PouchDoc2 {
    var id: String 
        get() = this.asDynamic()["_id"]
        set(v) { this.asDynamic()["_id"] = v}

    var rev: String?
        get() = this.asDynamic()["_rev"]
        set(v) { this.asDynamic()["_rev"] = v}
}

class Impl2 : PouchDoc2 {
    init {
        id = "id1"
        rev = "rev1"
    }
}

external interface PouchDoc3 { // marker interface 
}

var PouchDoc3.id: String 
    get() = this.asDynamic()["_id"]
    set(v) { this.asDynamic()["_id"] = v}

var PouchDoc3.rev: String?
    get() = this.asDynamic()["_rev"]
    set(v) { this.asDynamic()["_rev"] = v}

class Impl3 : PouchDoc3 {
    init {
        id = "id1"
        rev = "rev1"
    }
}

fun keys(a: Any) = js("Object").getOwnPropertyNames(a)

fun printKeys(a: Any) {
    println(a::class.simpleName)
    println(" instance keys: " + keys(a).toString())
    println("__proto__ keys: " + keys(a.asDynamic().__proto__).toString())
    println()
}

fun main(args: Array<String>) {
    printKeys(Impl1())
    printKeys(Impl2())
    printKeys(Impl3())
}
bashor
  • 8,073
  • 4
  • 34
  • 33
  • Thanks, that was informative. I found that Impl2 and Impl3 work but Impl1 still gets me mangled property names. I tried using a property delegate (as I'll describe in an answer-to-self below) but that got me into infinite recursion. A neater answer was over on the Kotlin forums at https://discuss.kotlinlang.org/t/controlling-the-jsname-of-fields-for-pouchdb-interop/2531 but that needed a bit of work, too. – HughG May 07 '17 at 08:45
0

I got a good answer from one of the JetBrains guys, Alexey Andreev, over on the JetBrains forum at https://discuss.kotlinlang.org/t/controlling-the-jsname-of-fields-for-pouchdb-interop/2531/. Before I describe that, I'll mention a further failed attempt at refining @bashor's answer.

Property delegates

I thought that @bashor's answer was crying out to use property delegates but I couldn't get that to work without infinite recursion.

class JSMapDelegate<T>(
        val jsobject: dynamic
) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return jsobject[property.name]
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        jsobject[property.name] = value
    }
}

external interface PouchDoc4 {
    var _id: String
    var _rev: String
}

class Impl4() : PouchDoc4 {
    override var _id: String by JSMapDelegate<String>(this)
    override var _rev: String by JSMapDelegate<String>(this)

    constructor(_id: String) : this() {
        this._id = _id
    }
}

The call within the delegate to jsobject[property.name] = value calls the set function for the property, which calls the delegate again ...

(Also, it turns out you can't put a delegate on a property in an interface, even though you can define a getter/setter pair which work just like a delegate, as @bashor's PouchDoc2 example shows.)

Using an external class

Alexey's answer on the Kotlin forums basically says, "You're mixing the business (with behaviour) and persistence (data only) layers: the right answer would be to explicitly serialise to/from JS but we don't provide that yet; as a workaround, use an external class." The point, I think, is that external classes don't turn into JavaScript which defines property getters/setters, because Kotlin doesn't let you define behaviour for external classes. Given that steer, I got the following to work, which does what I want.

external interface PouchDoc5 {
    var _id: String
    var _rev: String
}

external class Impl5 : PouchDoc5 {
    override var _id: String
    override var _rev: String
}

fun <T> create(): T = js("{ return {}; }")
fun Impl5(_id: String): Impl5 {
    return create<Impl5>().apply {
        this._id = _id
    }
}

The output of keys for this is

null
 instance keys: _id
__proto__ keys: toSource,toString,toLocaleString,valueOf,watch,unwatch,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,__defineGetter__,__defineSetter__,__lookupGetter__,__lookupSetter__,__proto__,constructor

Creating external classes

Three notes about creating instances of external classes. First, Alexey said to write

fun <T> create(): T = js("{}")

but for me (with Kotlin 1.1) that turns into

function jsobject() {
}

whose return value is undefined. I think this might be a bug, because the official doc recommends the shorter form, too.

Second, you can't do this

fun Impl5(_id: String): Impl5 {
    return (js("{}") as Impl5).apply {
        this._id = _id
    }
}

because that explicitly inserts a type-check for Impl5, which throws ReferenceError: Impl5 is not defined (in Firefox, at least). The generic function approach skips the type-check. I'm guessing that's not a bug, since Alexey recommended it, but it seems odd, so I'll ask him.

Lastly, you can mark create as inline, though you'll need to suppress a warning :-)

HughG
  • 1,108
  • 10
  • 14