Being reactive

Listeners and subscriptions

Kodein-DB supports the reactive pattern. You can use Kodein-DB as a data event hub, so that you can react to the addition or suppression of documents.

A listener is responsible for reacting to an operation.
Once you have registered it, you can get a subscription Closeable, which will stop the listener from being called if you close it.

Using the DSL

You can easily register a listener using the ad-hoc DSL:

DSL listeners
db.on<User>().register { (1)
}
db.onAll().register { (2)
}
1 Registers a listener on the User collection.
2 Registers a global listener to the entire database.

A DSL listener can access its own subscription (this can be useful if you want the listener to cancel its own subscription after reacting to a certain event) in the context of the callbacks:

Accessing the subscription
db.on<User>().register {
    didPut { user ->
        if (whatever) this.subscription.close()
    }
}

Using the DBListener interface

You can have one of your classes implement the DBListener interface and then register it:

Class listeners
class UserListener : DBListener<User> {}
class GlobalListener : DBListener<Any> {}

val uSub = db.on<User>().register(UserListener()) (1)
val aSub = db.onAll().register(GlobalListener()) (2)
1 Registers a listener on the User collection.
2 Registers a global listener to the entire database.

A class listener receives its own subscription (this can be useful if you want the listener to cancel its own subscription after reacting to a certain event) just after registration:

Receiving the subscription
class UserListener : DBListener<User> {
    lateinit var subscription: Closeable
    override fun setSubscription(subscription: Closeable) {
        this.subscription = subscription
    }
}

Before an operation

simple check

You can use the event system to act before an operation.

Any exception thrown in a will* callback cancels the operation (or batch of operation) and prevents subsequent callbacks to be called.

Reacting before an operation can be useful to ensure that the operation satisfies certain prerequisites, or to throw an exception to interrupt the operation if it isn’t.

A DSL check
db.on<User>().register {
    willPut { user ->
        check(user.name.isNotBlank()) { "User firstName and lastName must not be blank" }
    }
    willDelete {
        val pictureCount = db.find<Picture>().byIndex("userKey", key).entries().count()
        check(pictureCount == 0) { "User has pictures, delete them first" }
    }
}
A class check
class UserListener : DBListener<User> {
    override fun willPut(model: User, typeName: ReadMemory, metadata: Metadata, options: Array<out Options.Write>) {
        check(model.name.isNotBlank()) { "User firstName and lastName must not be blank" }
    }
    override fun willDelete(key: Key<User>, getModel: () -> User?, typeName: ReadMemory, options: Array<out Options.Write>) {
        val pictureCount = db.find<Picture>().byIndex("userKey", key).entries().count()
        check(pictureCount == 0) { "User has pictures, delete them first" }
    }
}

Accessing the deleted model

You may have noticed in the preceding example that the willDelete callback do not access the deleted model. That’s because it is not given to the willDelete DSL callback.
Because the deletion of a document uses its key, and not its model, you need to instruct the system to get the document before deleting it.

  • Using the DSL, use the willDeleteIt method:

    DSL delete check with model
    db.on<User>().register {
        willDeleteIt { user ->
            val pictureCount = db.find<Picture>().byIndex("userId", user.id).entries().count()
            check(pictureCount == 0) { "User has pictures, delete them first" }
        }
    }
  • Using the class method, call the getModel function argument:

    DSL delete check with model
    class UserListener : DBListener<User> {
        override fun willDelete(key: Key<User>, getModel: () -> User?, typeName: ReadMemory, options: Array<out Options.Write>) {
            val user = getModel()
            val pictureCount = db.find<Picture>().byIndex("userId", user.id).entries().count()
            check(pictureCount == 0) { "User has pictures, delete them first" }
        }
    }

After an operation

simple reaction

You can react after an operation, this can be useful:

  • Locally if you want to keep or a local state (such as a UI) up to date:

  • Globally if you want to keep a global state (such as the database itself) up to date.

Any exception thrown from a did* callback will not prevent other listeners to be called. Kodein-DB ensures that all did* listeners are called when an operation has suceeded.
A DSL reaction
db.on<User>().register {
    didPut { user -> ui.add(user) }
    didDelete { ui.reload() }
}
A class reaction
class UserListener : DBListener<User> {
    override fun didPut(model: User, key: Key<User>, typeName: ReadMemory, metadata: Metadata, size: Int, options: Array<out Options.Write>) {
        ui.add(model)
    }
    override fun didDelete(key: Key<User>, model: User?, typeName: ReadMemory, options: Array<out Options.Write>) {
        ui.reload()
    }
}

Note that all arguments of the listener’s methods are available in the DSL in the this context.

You can use didDelete to simulate cascading in a global listener:

DSL delete reaction with model
db.on<User>().register {
    didDelete {
        db.find<Picture>().byIndex("userKey", key).entries().forEach {
            db.delete(it.key)
        }
    }
}

Accessing the deleted model

You may have noticed in the preceding example that the didDelete callback do not access the deleted model. That’s because it is not given to the didDelete DSL callback, and will probably be null in the didDelete class method.
Because the deletion of a document uses its key, and not its model, you need to instruct the system to get the document before deleting it.

  • Using the DSL, simply use the didDeleteIt method:

    DSL delete reaction with model
    db.on<User>().register {
        didDeleteIt { user -> ui.remove(user) }
    }
  • Using the class method, call the getModel function argument in willDelete:

    DSL delete reaction with model
    class UserListener : DBListener<User> {
        override fun willDelete(key: Key<User>, getModel: () -> User?, typeName: ReadMemory, options: Array<out Options.Write>) {
            getModel()
        }
        override fun didDelete(key: Key<User>, model: User?, typeName: ReadMemory, options: Array<out Options.Write>) {
            ui.remove(model)
        }
    }

Informing listeners

Sometimes, you need to pass some context to the listener(s). Things like "Where is the operation coming from?" or "Why is this operation happening?". In short, you may need to inform your listeners about context.

For example, you may want to know if you are creating a new User, or updating one.

Doing so is easy. First, create a class that will hold the context and have it implement Options.Write:

A context class
enum class UserContext : Options.Write {
    NEW, UPDATE
}

Next, recover it from your listener:

Reading context in a listener
db.on<User>().register {
    didPut {
        val context = options.filterIsInstance<UserContext>().firstOrNull()
        when (context) {
            UserContext.NEW -> { /* insertion */ }
            UserContext.UPDATE -> { /* update */ }
            null -> { /* unknown */ }
        }
    }
}

Finally, don’t forget to add the context option when you perform the operation:

Adding context to a put.
db.put(newUser, UserContext.NEW)

Local reactions

You may need to attach a callback to a specific operation or batch of operation. For that, Kodein-DB provides the Anticipate and React options.

Regular

You can easily add a check that will run before an operation is performed (this is especially usefull for a batch):

Adding context to a put.
db.put(newUser,
        Anticipate { println("Will put a user!") },
        React { println("Did put a user!") }
)

db.newBatch().use { batch ->
    batch.addOptions(
            Anticipate { println("Will write batch!") },
            React { println("Did write batch!") }
    )
}