w3c / wot-scripting-api

Web of Things (WoT) Scripting API
http://w3c.github.io/wot-scripting-api/
Other
42 stars 28 forks source link

API improvements #78

Closed zolkis closed 6 years ago

zolkis commented 6 years ago

This issue is for discussing API improvements.

Let's start the discussion on observing functionality on the ConsumedThing interface.

There we have

Observable observe(DOMString name, RequestType requestType);

enum RequestType {  "property", "action", "event", "td" };

It allows subscribing and unsubscribing to notifications like:

Note that these could be also modeled with default events on ExposedThing, one being dedicated to 'descriptionchange', one for 'propertychange', one for 'actioninvocation' and then the signature would change to:

Observable observe(DOMString eventName);

but the functionality would be implicit.

I wonder which would be more developer-friendly.

mkovatsc commented 6 years ago

Where does the assumption come from that arbitrary Things do emit a notification when an Action was invoked? Is there any existing device already doing this?

zolkis commented 6 years ago

It comes from the early WoT scripting API. We also discussed it in calls. @knimura, IIRC you needed it?

zolkis commented 6 years ago

Apart from 'onactioninvoked', which design do you prefer?

zolkis commented 6 years ago

A simplified ConsumedThing interface based on the FPWD version, but taking into account also #68:

interface ConsumedThing {
    readonly attribute DOMString name;
    readonly attribute ThingDescription description;
    Promise<any>  invokeAction(DOMString name, any parameters);
    Promise<void> setProperty(DOMString name, any value);
    Promise<any>  getProperty(DOMString name);
    Observable    observe(DOMString name, optional ObserveOptions options);
};

callback ThingEventListener = void (ThingEvent event);

dictionary ObserveOptions {  // for conditional events
    callback filter = boolean (optional any value);
    // property-value pairs for constraints, defined by the TD
};

interface ThingEvent: Event {
    readonly attribute any data;
};
zolkis commented 6 years ago

Alternatively, taking into account #64, ConsumedThing can provide an object representation of a Thing where Thing properties are mapped as object properties, Thing actions are mapped as functions, Thing Events are mapped as object events. In addition, Events can also be handled by observe() in order to support complex events such as conditional events, but this can be optional.

interface ConsumedThing {
    readonly attribute DOMString name;
    readonly attribute ThingDescription description;
    Promise<object> renderAsObject();
    Observable observe(DOMString name, optional ObserveOptions options);
};

On WoT object level we'd have (taking into account #69):

interface WoT {
    Observable<ConsumedThing> discover(optional ThingFilter filter);
    Promise<ThingDescription> fetchDescription(USVString url);
    Promise<ConsumedThing> consume(ThingDescription description); 
    Promise<ExposedThing>     expose(ThingInit init);  // to be discussed later
};

@draggett , @danielpeintner, @knimura , @mkovatsc : opinions?

mkovatsc commented 6 years ago

Thank you for laying out these alternatives!

Overall, I see the assumption that the Scripting API, and in this case even the client API, can define which Interactions Things will offer as highly critical. This is leading to a Scripting API that is detached from the TD model and the reality of existing platforms (e.g., most devices do not provide notifications when an Action is invoked; if they do, it would become an Event in the TD). You are going down the path of a prescriptive standard that just becomes yet another standard out there.


Starting from the TD model, I would expect the following interface (here with Promises to deal with the distributed system aspects):

interface ConsumedThing {
    readonly attribute DOMString name;
    readonly attribute ThingDescription description;
    Promise<any>  readProperty(DOMString name); // aligned with 'writable'
    Observable    observeProperty(DOMString name);
    Promise<void> writeProperty(DOMString name, any value); // aligned with 'writable'
    Promise<any>  invokeAction(DOMString name, any parameters);
    Observable    subscribeEvent(DOMString name);
};

I am missing how the ObserveOptions should work, because the TD cannot define any constraints (so far).


For the WoT object, I would see this as simple:

interface WoT {
    Observable<ConsumedThing> discover(optional ThingFilter filter);
    Promise<ThingDescription> fetchDescription(USVString url);
    Promise<ConsumedThing>    consume(ThingDescription description); 
    Promise<ExposedThing>     expose(ThingDescription description); // maybe expose(ThingTemplate description)
};

The ThingInit basically had either only a name or already a description template. What we still need to figure out is the type ThingDescription. Having it as a string is pretty poor. Having it as object would need our famous pure JSON serialization, as JSON-LD does not work as JS object.

Also, ThingDescription would need to be a super type for either a TD instance (all links with network addresses etc.) for consume or a TD template (no links and bindings at all). Or we make Promise<ExposedThing> expose(ThingTemplate description);. The ThingDescription would correspond to HTML, the ThingTemplate to PHP/JSP/etc., maybe even with server-side code included.

Overall, I would like to get a programmatic way to construct TDs with the Scripting API (like we have with addProperty() etc., see #71 for the declaration problem).


Promise<object> renderAsObject();

The actual problem is then only delegated to that object: how should it work? We cannot use plain object properties and functions for distributed Things. Ideally, the ConsumedThing itself should already be that object. Maybe we can see how far we get with JavaScript's async/await?

zolkis commented 6 years ago

Overall, I see the assumption that the Scripting API, and in this case even the client API, can define which Interactions Things will offer as highly critical.

No, it just follows the already agreed representation and interactions: Thing, properties, actions, events. I don't see what do you fear and from where did you draw your conclusion.

This is leading to a Scripting API that is detached from the TD model and the reality of existing platforms (e.g., most devices do not provide notifications when an Action is invoked; if they do, it would become an Event in the TD).

What makes it detached from TD model? It provides a mechanism that is optional to use by scripts if the TD defines them and the policy how to use it. I agree that all notifications could be modeled by events, but that includes property changes as well...

You are going down the path of a prescriptive standard that just becomes yet another standard out there.

Another far fetched conclusion, would you please take the time to explain with concrete terms? What is prescriptive here? And what is the difference between "normative" (as per W3C specification) and your term "prescriptive"?

Observable subscribeEvent(DOMString name);

The name is wrong, Observable already has a subscribe() method. Let's stick to observe().

Observable    observeProperty(DOMString name);

Why prescribe that you can observe a property when the TD eventually hasn't defined that interaction? :) Isn't this leading to just another standard that is detached from the TD? And why cannot this be modeled as an event?

To be clear, I didn't ask for watching action invocation, it was raised by others (and IIRC actually I had to put it back after once I removed it from the spec), but I can see why a business logic would want that information. If you say then the TD should define it as an event I would say okay, but then let's also model other notifications as TD events as well. The way you've put it doesn't seem consistent.

Promise<ExposedThing>     expose(ThingDescription description); 

As I mentioned, this is postponed to a later discussion. Let's fix ConsumedThing first. That is why I left it with the FPWD signature. However, I agree with this signature.

Having it as a string is pretty poor. Having it as object would need our famous pure JSON serialization, as JSON-LD does not work as JS object.

I agree. But we have discussed during the past calls that at the moment we can't do better than representing it as a string(-serialized form). Should this change (with TD getting more maturity), we will change the representation. At this moment the apps don't care how the TD is written, it is the implementation's problem.

Or we make Promise expose(ThingTemplate description);

This is fine, if you want to make a distinction between a ThingTemplate that may contain [links to] scripts vs ThingDescription. We should document TD vs TT very well. Any links?

I would like to get a programmatic way to construct TDs with the Scripting API

That is also a valid requirement, however, we should not mix declarative and programmatic definition of a TD. The way I see it we should first do the simpler API above, and define a separate interface for programmatic definition of a TD (actually ThingTemplate).

The actual problem is then only delegated to that object: how should it work? We cannot use plain object properties and functions for distributed Things.

Why not? There is quite direct mapping between Thing and object. Just mentioning this topic has also been discussed in #64.

Ideally, the ConsumedThing itself should already be that object.

Then we run into namespacing problems, as mentioned in #64.

Maybe we can see how far we get with JavaScript's async/await?

Hmm.. I don't get what this has to do with the above discussion?

mkovatsc commented 6 years ago

No, it just follows the already agreed representation and interactions: Thing, properties, actions, events. I don't see what do you fear and from where did you draw your conclusion.

Actions are not able to emit Events when invoked. It is not modeled in the TD, nor implemented on a range of existing platform. You cannot add this feature on the client side; it would have to be implemented on the server side, which is not necessarily an ExposedThing, but an actual device augmented with a TD.

What makes it detached from TD model? It provides a mechanism that is optional to use by scripts if the TD defines them and the policy how to use it.

You are adding a lot of features that are not part of the TD. The Scripting API should have a stable core first before we open up that much exploration. Moreover, the charter explicitly states that the Scripting API must follow the TD; I now can see why some Members were so eager about this.

Another far fetched conclusion, would you please take the time to explain with concrete terms? What is prescriptive here? And what is the difference between "normative" (as per W3C specification) and your term "prescriptive"?

By presecriptive the WoT WG means that many platforms standardize what must be implement on the devices and how. To not become yet another standard, W3C WoT sees devices as fixed black boxes that can be described by a TD. When you start expecting a message from a device when a client called an Action, then this will only work by prescribing the device to implement this. This is all nice when you define your world only by Consumed- and ExposedThings. But not all Things will be ExposedThing software objects.

Why prescribe that you can observe a property when the TD eventually hasn't defined that interaction? :)

And there being a definion for setProperty() is working for you, although TDs might have all 'writable' flags set to false?

The name is wrong, Observable already has a subscribe() method. Let's stick to observe().

You still have not understood the point about Change-of-Value notifications of Properties vs stand-alone Event notifications of Events. The different words "observe" and "subscribe" shall express the different philosophies of Change-of-Value notifications like CoAP Observe and stand-alone Events of PubSub, respectively. The give you different behavior and guarantees that a developer of a distributed system has to be aware of.

Maybe we can see how far we get with JavaScript's async/await?

Hmm.. I don't get what this has to do with the above discussion?

This is an API for a distributed system. Naively assigning a value to an object property, expecting it to have crossed the Internet and at any point reading it out again just does not work in JavaScript. This is the reason why the first approach uses get/setProperty() and Promises. With async/await we might get a bit closer to the holy grail of distributed systems APIs.

zolkis commented 6 years ago

Actions are not able to emit Events when invoked.

I was not aware of that requirement. Can you quote the source? (I am not against it, just wondering about WoT requirement management). Why did node-wot have the emitEvent() function then?

It is not modeled in the TD,

Well, it may be modeled in the TD. The API just provided a closure.

nor implemented on a range of existing platform.

Well, actions are not implemented on a range of existing platform, either.

You cannot add this feature on the client side; it would have to be implemented on the server side,

Of course. All the time we've been discussing server API here.

which is not necessarily an ExposedThing, but an actual device augmented with a TD.

Currently I am not aware of any solution or even mechanism for that outside of ExposedThing - or please provide references. The whole purpose of ExposedThing was to enable augmenting a device with a TD... for instance by representing a device in a WoT runtime and exposing it with a TD.

You are adding a lot of features that are not part of the TD.

It's not me: these features were there in the original IG API under your watch...

By presecriptive the WoT WG means that many platforms standardize what must be implement on the devices and how.

Ok, we agree on that point then. However, having a mechanism in scripting API is not prescriptive, since it can fail and notifies the script about that.

That of course doesn't mean we should not clean up the API, just mentioning it is not prescriptive.

But not all Things will be ExposedThing software objects.

Of course not, but when they are, they could actually be represented as objects.

Naively assigning a value to an object property, expecting it to have crossed the Internet and at any point reading it out again just does not work in JavaScript. This is the reason why the first approach uses get/setProperty() and Promises.

In principle I agree if it was for live objects. I don't exactly know what @draggett had in mind - but also don't forget implementations can actually start a transaction and block until completion (there is no UI), or could use shard sync on that property, recording the change intents and resolving them in the background, while providing confirmed values on reads. I'd agree it would be too much twisting for the simplicity, and I have no problem writing a few more characters for calling an asynchronous setter instead of just assigning an object property.

What the object representation would solve is more like introspection of the represented TD, for which we don't yet have a solution (see #38). Of course that can be solved in another way - in the future.

mkovatsc commented 6 years ago

Actions are not able to emit Events when invoked.

I was not aware of that requirement. Can you quote the source? (I am not against it, just wondering about WoT requirement management). Why did node-wot have the emitEvent() function then?

emitEvent() has been the stub to implement the Event Interaction Pattern.

If a Thing wants notify other Things that an Action was invoked, it has to implement this explicitly by adding an Event Interaction and connecting the two Interactions through application code. By having an Action Interaction alone, it cannot do it. Actions are not subscribable.

Well, actions are not implemented on a range of existing platform, either.

Action is an abstract interaction concept for invoking some activity on the target. All RPC-style platforms implement Actions.

It feels like you are to fixated on the implementation through the Scripting API. The Scripting API is optional. The concepts must come from our Thing abstraction and the TD, and not from what is possible in JavaScript when programming a green field.

zolkis commented 6 years ago

Except that one cannot be sure what the TD will support in the future, and while you can break a TD, it is best to be avoided in scripting API.

Again, action notifications are not my idea, they've been present before the WG, and I am not sure if the above is group decision or your opinion. We need to clarify that during the next F2F.

However, the best test for whether do we really need a feature is dropping it...

So the current assumption on the simplified ConsumeThing interface:

interface ConsumedThing {
    readonly attribute DOMString name;
    readonly attribute ThingDescription description;
    Promise<any>  readProperty(DOMString name); // aligned with 'writable'
    Promise<void> writeProperty(DOMString name, any value); // aligned with 'writable'
    Promise<any>  invokeAction(DOMString name, any parameters);
    Observable    observe(DOMString name);  // event / property change notifications
};

Why can observe() handle both WoT Events and WoT Property Change: subscribing to an Observable requires callbacks for handling next value, error and completion. They can model events, a stream of values with various constraints, or simple changes, including the (claimed) differences between WoT Events and WoT Property Change. Differentiation is by name: if name matches a property on the Thing, then it is a value change notification. If it matches an event defined in the TD, then it is an event notification. The implementation will use the appropriate error handling policy specified in the algorithm (coming in the near future).

Before you equate observe() with CoAP observe, let me say they are NOT the same thing :).

We should discuss in the F2F whether the API should split property change notifications and WoT Event handling.

mkovatsc commented 6 years ago

There have not been any Action notifications. Good that we can now keep it this way :)

mkovatsc commented 6 years ago
interface ConsumedThing {
    readonly attribute DOMString name;
    readonly attribute ThingDescription description;
    Promise<any>    readProperty(DOMString name); // aligned with 'writable'
    Promise<void>   writeProperty(DOMString name, any value); // aligned with 'writable'
    Promise<any>    invokeAction(DOMString name, any parameters);
    Observable<any> observe(DOMString name);  // event / property change notifications
};

+1 for its simplicity. Let's discuss this proposal at TPAC, I guess including:

interface WoT {
    Observable<ConsumedThing> discover(optional ThingFilter filter);
    Promise<ThingDescription> fetchDescription(USVString url);
    Promise<ConsumedThing> consume(ThingDescription description); 
//  Promise<ExposedThing>     expose(...);  // t.b.d.
};
zolkis commented 6 years ago

Yes, that is the proposal for ConsumedThings and let's see on the F2F if anyone will want action notifications and how the group decides about it.

About the WoT interface, IMHO we should start with

interface WoT {
    Observable<ConsumedThing> discover(optional ThingFilter filter);
    Promise<ThingDescription> fetchDescription(USVString url);
    Promise<ConsumedThing> consume(ThingDescription description); 
    Promise<ExposedThing> expose(ThingTemplate description);
};

ThingTemplate to be defined...

and ExposedThing concept with building and running interface defined by different interfaces, but used on the same object when implemented.

interface ExposedThing {
    // define how to expose and run the Thing
    Promise<void> register(optional USVString url);
    Promise<void> unregister(optional USVString url);
    Promise<void> start();
    Promise<void> stop();
    Promise<void> emitEvent(DOMString eventName, any payload);
};

interface ThingBuilder {
    readonly attribute ThingDescription description;
    // define Thing Description modifiers, return itself for chainability
    ThingBuilder  addProperty(ThingPropertyInit property);  // throws on error
    ThingBuilder  removeProperty(DOMString name);  // throws on error
    ThingBuilder  addAction(ThingActionInit action);  // throws on error
    ThingBuilder  removeAction(DOMString name);  // throws on error
    ThingBuilder  addEvent(ThingEventInit event);  // throws on error
    ThingBuilder  removeEvent(DOMString name);  // throws on error
};

ExposedThing implements ConsumedThing;
ExposedThing implements ThingBuilder;  // can be optional in the beginning
};

dictionary ThingPropertyInit {
    DOMString name;
    boolean writable;
    boolean observable;
    PropertyType type;
    sequence<SemanticType> semanticTypes;
    any value;
    callback onwrite = Promise<void> (any value);
};

The onwrite callback could also be optional parameter to the addProperty function instead of being dictionary member.

We need to define PropertyType and work more on ThingActionInit (arg and return types) in order to avoid using TD fragments (that defy the purpose of ExposedThing, namely to programmatically define a TD, and not just assemble from TD fragments).

mkovatsc commented 6 years ago

What is PropertyType type;? The data type? The type of that object would be the same as for inputData and outputData of Actions...

I would have expected a boolean observable;.

callback onread is not required. We only need to act upon changes. Ideally, Property state can be served directly from the runtime implementation without calling the script.

zolkis commented 6 years ago

Yes, property type, action return type, and action arg types need to be defined programmatically.

Yes, we can add observable.

Yes, the implementation can do onread, but the question is can a script hook on that (and property updates).

Modified the proposal according to the suggestions.

zolkis commented 6 years ago

According to the TD type system, the mapping to JS/TS would be any (boolean, integer, number, string, array, object).

When we define that type TD fragment with a script, the script needs to provide the implementation a constructor, serialization/deserialization and a validator for property value.

So one possibility is to represent TD types (property type, action input data and output data) as follows:

dictionary ThingData {
    any value;
    ThingDescription description;
    callback construct = any (any input);  // create from JS value
    callback fromString = any (USVString input);  // create from serialized form
    callback toString =  USVString (any input);  
    callback isValid = boolean (any input);
}
danielpeintner commented 6 years ago

I like the simple way of defining ConsumedThing (here above). Having said that, we might need to check whether replacing all listeners with observe is applicable, given that the Observable pattern is just proposed to be included in ECMAScript.

Moreover, with regards to ExposedThing there seems to be consensus how to build a Thing (see ThingBuilder) and how to expose and run a thing. The part which is still open is how to define request handlers which is tackled in Issue https://github.com/w3c/wot-scripting-api/issues/72.

Anyhow, I think we have a good starting point to discuss this at TPAC.

zolkis commented 6 years ago

he part which is still open is how to define request handlers

In the proposal above request handlers are defined

We can discuss in today's call which is preferable.

danielpeintner commented 6 years ago

Ohh, I missed the onwrite callback in ThingPropertyInit! Thanks.

zolkis commented 6 years ago

I think we should align addProperty() with defineProperty(), so the signature would be:

partial interface ThingBuilder {
    ThingBuilder  addProperty(DOMString name, ThingPropertyInit property);  // throws on error
};

dictionary ThingPropertyInit {
    boolean writable;
    boolean observable;
    ThingDataType type;
    sequence<SemanticType> semanticTypes;  // could be included in type???
    any value;
    callback set = Promise<void> (any value, optional ThingDataOptions options);  
    callback get = Promise<any>(optional ThingDataOptions options);  
};

[Constructor(DOMString name, optional ThingDataOptions options)
interface ThingDataType: ThingDataOptions {
    DOMString name;
    callback fromValue = any (any input);  // create from JS value
    callback fromString = any (USVString string);  // create from serialized form
    callback isValid = boolean (any value, optional ThingDataOptions options);
};

dictionary ThingDataOptions {  // examples of constraints defined in a TD
    DOMString units;
    (double or long) minValue;
    (double or long) maxValue;
    double minFrequency;
    double maxFrequency;
    double frequency;
}
mkovatsc commented 6 years ago

Just to capture some points from today's Scripting API call:

mkovatsc commented 6 years ago

On Property value generator functions:

In node-wot, the pattern has been so far that some sampling algorithm uses setPropert() locally to update the value that can be read remotely through the WoT Interface. So a generator function is not absolutely needed, but might allow for more efficient implementations.

zolkis commented 6 years ago

Capturing one main use case for the ExposedThing API: a script consumes Things and defines a new ExposedThing based on the consumed Things [and local information]. For that, the script may a) create an "empty" ExposedThing and then modify it (modify/add/remove properties/actions/events) b) create an ExposedThing based on a TD and then modify it.

Adding a property/action/event to an ExposedThing from another ConsumedThing assumes the script is able to identify the corresponding TD fragment (opaque string or object) that it wants to copy/paste in the new ExposedThing.

Adding a new property (without a TD fragment at hand) means the script needs to be able to define the TD data type of the property (ThingBuilder.addProperty()). This is what seems to be complex. An experimental solution might be to pass a JSON object to initialize that property. The developer can produce that object/string with an external tool/API/algorithm based on a domain/solution specific knowledge. The implementation will take the JSON object and translate it to a TD fragment by an algorithm that is specified in the Scripting API spec.

zolkis commented 6 years ago

In order to decide what depth the API should cover, and what are the priorities:

In the use case above a developer wants to consume a few Things and expose a new Thing as a combination specific to the business logic.

For this, the developer needs to:

  1. have access to the TDs of the consumed Things, for instance a) download the TDs as text files b) write a script using the Scripting API that instantiates ConsumedThing objects

  2. assemble a new TD, for instance by a) copy/pasting the TD fragments in a text editor b) add code to the script from 1b that creates an ExposedThing object and adds properties/actions/events programmatically, by invoking functions and specifying TD fragments

      i. in JavaScript
      ii. as TD fragments (opaque text). 
  3. Expose the new Thing by a) writing a script that calls an API function that given a TD creates an ExposedThing, then call start() and register() on it b) calling start() and register() on the previously created ExposedThing. c) calling an action on a remote Thing with the new TD. The action instantiates an ExposedThing, starts and registers it automatically.

  4. Deploy and run scripts, e.g. by calling an action on a remote Thing a) the script defined in 3a b) the script defined in 1b

The sensible development paths are:

Note that in the first development path we do not need any ExposedThing API, just WoT and ConsumedThing. We just assemble a new TD by a tool (text editor) and call an action on a remote Thing to consume the given new TD. This is the simple case when default request handlers are fine.

The first path is the simplest, followed by the second. This is what we should pursue in the first version of the Scripting API. Note that specifying handlers to the ExposedThing could be done in the TD, by adding a new fragment that specifies language binding (e.g. JavaScript or TypeScript) and source code.

Then based on further experience with this use case, we may consider adding support for the 1b --> 2b-ii --> 3b development path (with ThingBuilder interface). Note that in step 2b, the FPWD spec uses the 2b-ii variant, whereas my proposal above uses 2b-i variant. For 2b-ii may be possible, 1b needs support for a functionality that splits a TD in reusable TD fragments and provides introspection (issue #38).

The second development path needs the following API:

typedef USVString ThingDescription;

interface WoT {
    Observable<ConsumedThing> discover(optional ThingFilter filter);
    Promise<ThingDescription> fetchDescription(USVString url);
    Promise<ConsumedThing> consume(ThingDescription description); 
    Promise<ExposedThing> expose(ThingDescription description);
};

interface ConsumedThing {
    readonly attribute DOMString name;
    readonly attribute ThingDescription description;
    Promise<any>    readProperty(DOMString name); // aligned with 'writable'
    Promise<void>   writeProperty(DOMString name, any value); // aligned with 'writable'
    Promise<any>    invokeAction(DOMString name, any parameters);
    Observable<any> observe(DOMString name);  // event / property change notifications
};

interface ExposedThing {
    // define how to expose and run the Thing
    Promise<void> register(optional USVString url);
    Promise<void> unregister(optional USVString url);
    Promise<void> start();
    Promise<void> stop();
    Promise<void> emitEvent(DOMString eventName, any payload);
};

The second development path needs ExposedThing and eventually/optionally supporting specifying other-than-default request handler code in a TD. If that is not possible, we can add hooks to ExposedThing:

callback PropertyReadHandler = Promise<any> (DOMString name);
callback PropertyWriteHandler = Promise<void> (any value);

partial interface ExposedThing {
    ExposedThing setReadHandler(DOMString propertyName, PropertyReadHandler handler);
    ExposedThing setWriteHandler(DOMString propertyName, PropertyWriteHandler handler);
}

Note that we don't need event app-specific emitter handlers, since emitEvent() is called with the event name (defined in the TD) and the payload data (defined by the script), therefore it is flexible enough.

We should discuss at the F2F whether we need to formalize support for notifications when an action was invoked.

The 3rd development path will need a version evolved from the current (FPWD) spec.

mkovatsc commented 6 years ago

There was feedback at the TPAC meeting that it would be more intuitive to have explicit calls for observing a Property and subscribing to Events, as Property and Event are two different concepts:

Observable<any> observeProperty(DOMString name);  // Observable Property
Observable<any> subscribeEvent(DOMString name);  // Event
zolkis commented 6 years ago

Yes, I know you have mentioned it earlier, too, and then I responded that subscribeEvent is not a good name since Observable contains a subscribe() method. It should be observeEvent() in order to be consistent.

If the algorithms are not different (should they?) and a property name cannot clash with an event name (shall not), then a single method is just simpler, as it conveys developer interest to observe a name declared in the TD, that corresponds either to a property name or an event name. (The topic I would like to raise though is to check at TPAC who wants observing actions supported, since this was clearly in the API before, and I would like this use case get clarified.)

Until the algorithms are defined, we could keep separate methods.

mkovatsc commented 6 years ago

I am wondering where the convention for the observe() function comes from. Why first get the Observable, on which subscribe() has to be called again?

Would a better use of the Observable pattern be:

Subscription observeProperty(DOMString name, SubscriptionObserver);
Subscription subscribeEvent(DOMString name, SubscriptionObserver);

(see https://github.com/tc39/proposal-observable for Subscription and SubscriptionObserver)

zolkis commented 6 years ago

More and more complex. Let's stick with Observable as it meant to be used. You need the subscribe() method to pass the onNext, onError, onCompletion callbacks.

mkovatsc commented 6 years ago

I don't see what is complex. subscribeEvent() directly becomes the subscribe() function that takes onNext, onError, onCompletion.

Nowhere I could find usuage of an observe() function to get an Observable first. The ExposedThing vasically becomes the Observable.

zolkis commented 6 years ago

It's more complex to use than the version returning the Observer. It is not ExposedThing but the event that is modeled by the Observable. What would exposedThing.subscribe() do? I suggest reading again the Observable proposal.

ektrah commented 6 years ago

In the .NET world, where Rx originates, observables implementing the IObservable\<T> interface can be filtered, transformed and composed with other observables. You then subscribe the observer to the filter/​transformation/​composition, not to the original observable. If it's similar in ECMAScript, it makes a lot of sense to have something like getEvent(name).subscribe(observer).

mkovatsc commented 6 years ago

The main issue I have is probably using the name "observe" in the function, because it is synonymous to subscribe. Something like @ektrah 's getEvent makes it clearer.

zolkis commented 6 years ago

We can use the

getEvent(name: string): Observable

method for events. It throws if name is not an event.

For observing properties we can have a different method then, what should be the name? observe(propertyName) works for me, observeProperty(name) is more specific (hence more clear), so maybe that. For now.

We can start specifying the new API after the TPAC.

ektrah commented 6 years ago

A stream of property change events is just another observable that you might want to filter/​transform/​compose. So it would make sense to have something like getProperty(name).subscribe(observer). The observable could also have the methods for getting and setting the property value, e.g.:

value = await thing.getProperty("some_property").get();
await thing.getProperty("some_property").set(value);
thing.getProperty("some_property").subscribe(observer);

More concisely:

await thing["some_action"]();

value = await thing["some_property"].get();
await thing["some_property"].set(value);
thing["some_property"].subscribe(observer);

thing["some_event"].subscribe(observer);

Or even just:

await thing.some_action();

value = await thing.some_property.get();
await thing.some_property.set(value);
thing.some_property.subscribe(observer);

thing.some_event.subscribe(observer);
zolkis commented 6 years ago

@ektrah

The observable could also have the methods for getting and setting the property value

So we extend Observable to a PropertyObservable with get() and set() and subscribe()?

Also, in order to address action tracking discussions, I am thinking about the possibility to extend this pattern to action handling as well, i.e. get an action as Observable, then run it (equivalent to subscribing to it) with the 3 callbacks (next() to track progress, complete(), error()). Also, it would allow to request canceling an action (when supported, and if it has not completed yet).

Then we could have

property = thing.property("propertyName");
propertyValue = await property.get();
propertySubscription = property.subscribe(propertyObserver);
// ...
eventSubscription = thing.event("eventName").subscribe(eventObserver);
// ...
actionSubscription = thing.action("actionName").run(actionObserver);
// ...

Where Observer defines next(), error(), complete(), optionally start().

So ConsumedThing becomes:

interface ConsumedThing {
    readonly attribute DOMString name;
    readonly attribute ThingDescription description;
    PropertyObservable property(DOMString name);
    ActionObservable action(DOMString name);
    Observable event(DOMString name);
};

interface PropertyObservable: Observable {
    Promise<any> get();
    Promise<void> set(any value);
    Subscription subscribe(observer);
};

interface ActionObservable: Observable {
    Subscription run(observer);
};
ektrah commented 6 years ago

So we extend Observable to a PropertyObservable with get() and set() and subscribe()?

subscribe is inherited from Observable, so only get() and set(value) would need to be added.

Also, in order to address action tracking discussions, I am thinking about the possibility to extend this pattern to action handling as well, i.e. get an action as Observable, then run it (equivalent to subscribing to it) with the 3 callbacks (next() to track progress, complete(), error()).

It's possible to interpret a promise as an event stream with one event. But in the .NET world, async/await has clearly won over observables for async operations with one result.

zolkis commented 6 years ago

It's possible to interpret a promise as an event stream with one event. But in the .NET world, async/await has clearly won over observables for async operations with one result.

The use case would be that

Clearly, a simple Promise would cover most use cases (simple actions).

The best solution may be that actions continue to return a Promise that normally resolve with the return value of the action for simple actions. Actions that can be canceled and progress-monitored resolve with an Observable. The TD of the action needs to be explicit on what is allowed from this list: cancel, next, error, complete and then the Observable exposes those methods. @sebastiankb could we have that supported in the TD?

The alternative to this would be that the action returns a Thing that implements Observable-like functionality, but that seems to be more complex to me.

zolkis commented 6 years ago

I realized that if we want to support cancellable actions, we need to use Observable since cancellable Promises are not coming to ECMAScript. However, it's strange to use an Observable for actions, where next() receives progress, complete() receives the return value, error() gets errors.

Sure, we could also use an explicit interface, if needed:

partial interface ConsumedThing {
    ActionRunner action(name);
};

interface ActionRunner
{
    readonly attribute boolean cancelable; // from TD: action may be canceled
    readonly attribute boolean trackable;  // from TD: progress events supported
    Promise<any> run(any parameters);  // handles errors + returning value 
    void cancel();  // successful cancel will make run() reject with "aborted" error
    attribute ProgressCallback onprogress;
}
callback void ProgressCallback(double);

So one could say

let action =  thing.action("openGarageDoor");
if (action.trackable) {
    action.onprogress = progress => console.log("progress: " + progress);
}
action.run("slow")  // TBD: may create a rollback point at the remote Thing
.then ( ... )
.catch ( ... );
// ... at some point
if (action.cancelable) {
    action.cancel();      // will also reject the promise above
    // TBD: should it roll back, i.e. restore previous door position
}

The same functionality using Observable would (arguably) be less intuitive to define.

let action = thing.action("openGarageDoor").subscribe({ 
    next(progress) { console.log("Progress: " + progress); },
    error(err) { ... },
    complete(value) { ... });
});

action.run("slow");

// at some point
actionHandle.cancel();

(Note that in this version subscribe() doesn't start the action yet, and I have moved run() to an extension of the Subscription interface).

A somewhat unrelated question is whether canceling an action also means rollback to the state before starting the action, or not. If there is no rollback, IMHO it would be better to say action.start() and action.stop() rather than action.run() and action.cancel(). @mjkoster?

mkovatsc commented 6 years ago

The best solution may be that actions continue to return a Promise that normally resolve with the return value of the action for simple actions. Actions that can be canceled and progress-monitored resolve with an Observable.

There continues to be a misconception about how Actions should work. In the general case, an Action is simply a message sent that might solicit a reply. Thus a Promise. We need to keep it more focused on the network-facing WoT Interface and what it allows us to do, as it is what is described by TDs.

The advanced case, where a progress is supposed to be monitored, the started task might be canceled etc., would require the WoT Server to return a message that tells us where we can read the progress information or invoke cancellation. Here, the hypermedia-driven view comes in: The reply might be something similar to a TD document that describes the running task. It can have (Observable) Properties to monitor progress, Actions to cancel, or Events to asynchronously inform about completion. A scripting API then should work on what the Promise returns. This has been the "HATEOAS" assumption within the WoT IG.

zolkis commented 6 years ago

A scripting API then should work on what the Promise returns.

And that means we need to expose/standardize an API that can do that, which is exactly the proposal. It doesn't matter how the runtime handles it in the background (which you have described).

Another way to model it is with a Promise that can resolve in anything. It is then business logic specific knowledge whether that action returns a Thing (for monitoring).

mkovatsc commented 6 years ago

A Promise that can resolve in anything would not prescribe how to implement Actions; what if someone wants to have multiple Properties associated to a task / running Action (-> need terminology entry). I also see how it requires more work in the business logic.

Yet an API tailored to complex Actions also complicates the simple case, where only a command message is expected to be sent...

zolkis commented 6 years ago

Addressed by #86.