Skip to content

Commit

Permalink
Wasm API Tests: Rendering in Kotlin (PolymerLabs#4047)
Browse files Browse the repository at this point in the history
* modify fake slotComposer to listen to new rendering system

* enabling first render test for kotlin

* Adapting render for null cases

* Updating render test -- now passing

* enable kotlin test for autoRender

* Specify render on handle sync and update

* Default particle handle behavior is now to renderOutput

* renderTest: more idiomatic kt

* fix: sp

* Documented Particle class
- added docs
- also converted {} to Unit

* reducing variables

* Added more comments to wasm interop

* update connectHandle to match ABI
- added Direction enum
- TODO(alxr): Make direction meaningful for connections

* Revert behavior: Handle update --> Rendering

* Autoformat

* Fixed LOTS of lint errors in interop

* fixed kt lint errors in tests

* Fixed particle lint erros

* revert import change; hopefully doesn't fail lint

* got ktlint and bazel build right
  • Loading branch information
alxmrs authored Nov 15, 2019
1 parent 47a3470 commit 6ae7f0a
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 80 deletions.
2 changes: 1 addition & 1 deletion src/runtime/particle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export class Particle {
}

/**
* Called for handles that are configued with notifyUpdate, when change events are received from
* Called for handles that are configured with notifyUpdate, when change events are received from
* the backing store. For handles also configured with keepSynced these events will be correctly
* ordered, with some potential skips if a desync occurs. For handles not configured with
* keepSynced, all change events will be passed through as they are received.
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/testing/fake-slot-composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {SlotComposer, SlotComposerOptions} from '../slot-composer.js';
import {SlotContext} from '../slot-context.js';
import {Particle} from '../recipe/particle.js';
import {Content} from '../slot-consumer.js';
import {Arc} from '../arc.js';

/**
* A helper class for NodeJS tests that mimics SlotComposer without relying on DOM APIs.
Expand Down Expand Up @@ -67,4 +68,11 @@ export class RozSlotComposer extends FakeSlotComposer {
this.received.push([particle.name, slotName, copy]);
super.renderSlot(particle, slotName, content);
}


/** Listener for experimental `output` implementation */
delegateOutput(arc: Arc, particle: Particle, content) {
const slotName: string = content && content.targetSlot && content.targetSlot.name || 'root';
this.received.push([particle.name, slotName, content]);
}
}
10 changes: 7 additions & 3 deletions src/runtime/wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,9 +848,13 @@ export class WasmParticle extends Particle {

// render request call-back from wasm
onRenderOutput(templatePtr: WasmAddress, modelPtr: WasmAddress) {
const content: Content = {templateName: 'default'};
content.template = this.container.read(templatePtr);
content.model = StringDecoder.decodeDictionary(this.container.read(modelPtr));
const content: Content = {};
if (templatePtr) {
content.template = this.container.read(templatePtr);
}
if (modelPtr) {
content.model = StringDecoder.decodeDictionary(this.container.read(modelPtr));
}
this.output(content);
}

Expand Down
10 changes: 5 additions & 5 deletions src/wasm/kotlin/src/arcs/Entity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class StringDecoder(private var str: String) {
val dict = mutableMapOf<String, String>()

var num = decoder.getInt(":")
while(num-- > 0){
while (num-- > 0) {
val klen = decoder.getInt(":")
val key = decoder.chomp(klen)

Expand All @@ -31,7 +31,7 @@ class StringDecoder(private var str: String) {
}
}

fun done():Boolean {
fun done(): Boolean {
return str.isEmpty()
}

Expand Down Expand Up @@ -85,15 +85,15 @@ class StringEncoder(private val sb: StringBuilder = StringBuilder()) {
val sb = StringBuilder()
sb.append(dict.size).append(":")

for((key, value) in dict) {
for ((key, value) in dict) {
sb.append(key.length).append(":").append(key)
sb.append(encodeValue(value))
}
return sb.toString()
}

fun encodeList(list: List<Any>): String {
return list.joinToString(separator = "", prefix = "${list.size}:") { encodeValue(it) }
return list.joinToString(separator = "", prefix = "${list.size}:") { encodeValue(it) }
}

fun encodeValue(value: Any?): String {
Expand All @@ -114,7 +114,7 @@ class StringEncoder(private val sb: StringBuilder = StringBuilder()) {
}
}

fun result():String = sb.toString()
fun result(): String = sb.toString()

fun encode(prefix: String, str: String) {
sb.append("$prefix${str.length}:$str|")
Expand Down
157 changes: 129 additions & 28 deletions src/wasm/kotlin/src/arcs/Particle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,55 +10,155 @@ abstract class Particle : WasmObject() {
private val toSync: MutableSet<Handle> = mutableSetOf()
private val eventHandlers: MutableMap<String, (Map<String, String>) -> Unit> = mutableMapOf()

/** Execute on initialization of Particle. */
open fun init() = Unit

/**
* Associate a handle name to a handle object.
*
* @param name Name of handle from particle in manifest
* @param handle Singleton or Collection, defined in this particle class
*/
fun registerHandle(name: String, handle: Handle) {
handle.name = name
handle.particle = this
handles[name] = handle
log("Registering $name")
}

fun eventHandler(name: String, handler: (Map<String, String>) -> Unit) {
eventHandlers[name] = handler
}

fun connectHandle(handleName: String, willSync: Boolean): Handle? {
log("Connect called internal '$handleName'")

handles[handleName]?.let {
if (willSync) toSync.add(it)
/**
* Connect to a registered handle
*
* If a handle has been previously registered, return the handle. Optionally, mark the handle for later
* synchronization.
*
* @param name Name of the handle
* @param canRead Mark handle with read access
* @param canWrite Mark handle with write access
* @return The name-associated handle, or null
* @see [registerHandle]
* @see [onHandleSync]
*/
fun connectHandle(name: String, canRead: Boolean, canWrite: Boolean): Handle? {
log("Connect called internal '$name'")

handles[name]?.let {
if (canRead) {
toSync.add(it)
it.direction = if (canWrite) Direction.InOut else Direction.In
} else {
it.direction = Direction.Out
}
return it
}

log("Handle $handleName not registered")
log("Handle $name not registered")
return null
}

/**
* Register a reaction to an event.
*
* Particle templates may emit events, usually from user actions.
*
* @param name The name of the triggered event
* @param handler A callback (consumer) in reaction to the event
*/
fun eventHandler(name: String, handler: (Map<String, String>) -> Unit) {
eventHandlers[name] = handler
}

/**
* Trigger an event.
*
* Will target registered events, if present. Will always initiate rendering when called.
*
* @param slotName Slot that the event is associated with; likely `root`
* @param eventName Name of the event to trigger
* @param eventData Data associated with the event; will be passed into event handler
* @see [eventHandler]
*/
open fun fireEvent(slotName: String, eventName: String, eventData: Map<String, String>) {
eventHandlers[eventName]?.invoke(eventData)
renderOutput()
}

/** @param handle Handle to synchronize */
fun sync(handle: Handle) {
log("Particle.sync called")
toSync.remove(handle)
onHandleSync(handle, toSync.isEmpty())
}

open fun onHandleUpdate(handle: Handle) {}
open fun onHandleSync(handle: Handle, allSynced: Boolean) {}
/**
* React to handle updates.
*
* Called for handles when change events are received from the backing store. Default action is to trigger
* rendering.
*
* @param handle Singleton or Collection handle
*/
open fun onHandleUpdate(handle: Handle) = Unit

/**
* React to handle synchronization.
*
* Called for handles that are marked for synchronization at connection, when they are updated with the full model
* of their data. This will occur once after setHandles() and any time thereafter if the handle is resynchronized.
* Default action is to trigger rendering.
*
* @param handle Singleton or Collection handle
* @param allSynced flag indicating if all handles are synchronized
*/
open fun onHandleSync(handle: Handle, allSynced: Boolean) = Unit

/** Rendering through UiBroker */
fun renderOutput() {
log("renderOutput")
val slotName = ""
val template = getTemplate(slotName)
val dict = populateModel(slotName)
val model = StringEncoder.encodeDictionary(dict)
onRenderOutput(toWasmAddress(), template.toWasmString(), model.toWasmString())
val model = populateModel(slotName)?.let { StringEncoder.encodeDictionary(it) }
onRenderOutput(
toWasmAddress(),
template.toWasmNullableString(),
model.toWasmNullableString()
)
}

/**
* @deprecated for contexts using UiBroker (e.g Kotlin)
* Define template for rendering (optional)
*
* @param slotName name of slot where template is rendered.
* @see [renderOutput]
*/
open fun getTemplate(slotName: String): String? = null

/**
* Populate model for rendering (UiBroker model)
*
* @param slotName name of slot where model data is populated
* @param model Starting model state; Default: empty map
* @return new model state
* @see [renderOutput]
*/
@Deprecated("Rendering refactored to use UiBroker.", ReplaceWith("renderOutput()") )
open fun populateModel(
slotName: String,
model: Map<String, Any?> = mapOf()
): Map<String, Any?>? = model

/** @deprecated for contexts using UiBroker (e.g Kotlin) */
@Deprecated("Rendering refactored to use UiBroker.", ReplaceWith("renderOutput()"))
fun renderSlot(slotName: String, sendTemplate: Boolean = true, sendModel: Boolean = true) {
log("ignoring renderSlot")
}

/**
* Request response from Service
*
* @param call string encoding of service name; follows `service.method` pattern
* @param args Key-value encoded arguments for service request
* @param tag Optionally, give a name to the particular service call
*/
fun serviceRequest(call: String, args: Map<String, String> = mapOf(), tag: String = "") {
val encoded = StringEncoder.encodeDictionary(args)
serviceRequest(
Expand All @@ -69,18 +169,22 @@ abstract class Particle : WasmObject() {
)
}

open fun fireEvent(slotName: String, eventName: String, eventData: Map<String, String>) {
eventHandlers[eventName]?.invoke(eventData)
renderOutput()
}
/**
* Process response from Service call
*
* @param call string encoding of service name; follows `service.method` pattern
* @param response Data returned from service
* @param tag Optional, name given to particular service call
*/
open fun serviceResponse(call: String, response: Map<String, String>, tag: String = "") = Unit

/**
* Resolves urls like 'https://$particles/path/to/assets/pic.jpg'.
*
* The `$here` prefix can be used to refer to the location of the current wasm binary:
* `$here/path/to/assets/pic.jpg`
*
* @param String URL with $variables
* @param url URL with $variables
* @return absolute URL
*/
fun resolveUrl(url: String): String {
Expand All @@ -89,17 +193,14 @@ abstract class Particle : WasmObject() {
_free(r)
return resolved
}

open fun init() {}
open fun getTemplate(slotName: String): String = ""
open fun populateModel(slotName: String, model: Map<String, Any?> = mapOf()): Map<String, Any?> = model
open fun serviceResponse(call: String, response: Map<String, String>, tag: String = "") {}

}

enum class Direction { Unconnected, In, Out, InOut }

abstract class Handle : WasmObject() {
lateinit var name: String
lateinit var particle: Particle
var direction: Direction = Direction.Unconnected
abstract fun sync(encoded: String?)
abstract fun update(added: String?, removed: String?)
}
Expand Down
Loading

0 comments on commit 6ae7f0a

Please sign in to comment.