-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discovery API - Alternatives #364
Comments
Scripting Call 2022-01-17
|
I have been thinking about this proposal and I like it. It well aligns with what we have for w.r.t. "power" I think one can achieve the same, e.g.,
|
@danielpeintner Thank you for the feedback!
I would think so, too :) The only aspect that is probably not covered at the moment is pagination, but maybe this could be handled by adding additional parameters to the Also, I had two ideas which could probably be further improvements to the approach from above. The first one would be to add a The other idea I had was to turn the async function handleDiscovery(thingDescription: ThingDescription, stop: () => void) {
if (thingDescription.actions.has("foo") {
stop();
const consumedThing = await wot.consume(thingDescription);
await consumedThing.invokeAction("foo");
}
}
const discovery = wot.discover(handleDiscovery, { method: "directoy", url: "coap://example.org/tdd" );
// ... some time passes, then the discovery process is started again
discovery.restart();
// Then the discovery process is cancelled "manually".
discovery.stop(); |
Some questions...
What is the difference of having
The alternative would be to create yet another |
The additional let discovery;
async function handleDiscovery(thingDescription: ThingDescription) {
if (discovery != null) {
discovery.stop();
}
...
}
discovery = wot.discover(handleDiscovery, { ... ); This
That would also be an option :) I thought that |
I've been thinking about the |
For API design here, let's also keep in mind browser event loop considerations. Probably known content, but anyway :). Edit. For instance, we should check every place we call callbacks in the spec, as they need to move to "queue a task for..." constructs for that. |
relates to #222 |
Scripting Call 2022-01-31
|
A possible variant for a callback based discovery API uses the Subscription object, that already has an callback DiscoveryHandler = undefined(ThingDescription);
partial namespace WOT {
Subscription discover(DiscoveryHandler handler, optional ThingFilter filter = null);
};
typedef DOMString DiscoveryMethod;
dictionary ThingFilter {
DiscoveryMethod method = "directory";
USVString? url;
object? fragment;
}; |
A possible API with direct functions. callback DiscoveryHandler = undefined(ThingDescription);
partial namespace WOT {
[SecureContext, Exposed=(Window,Worker)]
interface WotDiscovery {
Promise<ThingDescription> direct(USVString url);
Promise<Subscription> directory(DiscoveryHandler handler, USVString url);
}; With examples like: let discovery = new WotDiscovery();
let td = await discovery.direct("https://mythings.my/thing2");
let subs = await discovery.directory(handler, "https://directory.mythings.my/");
...
if (subs.active) {
await subs.stop();
} That allows for a small variation. callback DiscoveryHandler = undefined(ThingDescription);
partial namespace WOT {
[SecureContext, Exposed=(Window,Worker)]
interface WotDirectDiscovery {
Promise<ThingDescription> start(USVString url);
};
[SecureContext, Exposed=(Window,Worker)]
interface WotDirectoryDiscovery extends Subscription {
Promise<undefined> start(DiscoveryHandler handler, USVString url);
}; The example becomes: let discovery = new WotDirectDiscovery();
let td = await discovery.start("https://mythings.my/thing2");
let discovery = new WotDirectoryDiscovery();
await discovery.start(handler, "https://directory.mythings.my/");
...
if (discovery.active) {
await discovery.stop();
} Edit: changed the direct discovery to a promise, as we don't need a handler there, and it also manages the errors. @relu91 PTAL |
Yet another variation. I like that So it might be that the current discovery API is still the most idiomatic and flexible:
Now we have a lot of options to discuss :). |
I agree that the current API can cover the points above. Still, it is an overkill for the simplest use-case:
I agree there's still a lot to discuss, taking also in mind that the Discovery specification is still ongoing (even though close to the feature freeze). |
Handling a list on the client side is best done with iterators, since this can encapsulate buffering, unlike when we expose arrays of TDs to the scripts. Providing a callback (two, actually) is another pattern, but splitting into We could make Example 9 and Example 10 with all alternatives as see which works best. |
I guess such an iterator would/should be an async one, right? It seems to me as if an Async Iterable could solve the blocking issue I mentioned above very elegantly. |
Yes, we could use an async iterator on the discovery object, they are supported in ECMAScript. |
Okay, great :) Do I see it correctly that we could then handle discovery like so? const discovery = WoT.discover({ method: "directory", url: "http://directory.example.org" });
for await (const thingDescription of discovery) { // Note: Has to be within an async function
console.log("Found Thing Description for " + thingDescription.title);
} If so, I think this would be a very nice solution. |
Today's Scripting call
|
Yes, the implementations need to handle if (discovery.active) {
await discovery.stop();
} |
@JKRhb are you planning to experiment with iterators in the implementation? |
Sure, I will try to come with an example based on node-wot until the next meeting which could serve as a basis for the new API. |
I already tried to experiment with Iterators a bit, and it seems to if an You can experiment with the code yourselves in the Typescript Playground under this link. interface ThingDescription {
title: string
}
class ThingDiscovery {
active: boolean = true;
async *discover(uri: string): AsyncGenerator<ThingDescription> {
while (this.active) {
const response = await fetch(uri);
const parsedTd = await response.json();
yield new Promise<ThingDescription>(resolve => resolve(parsedTd));
}
}
public stop() {
this.active = false;
}
}
// Just a random URI pointing to a TD
const uri = "https://raw.githubusercontent.com/w3c/wot-testing/main/events/2022.03.Online/TD/hitachi-esp-idf/TDs/hitachi-acc-air.td.jsonld";
async function startDiscovery() {
let thingDiscovery = new ThingDiscovery();
let counter = 0;
for await (let thingDescription of thingDiscovery.discover(uri)) {
if (counter === 3) {
thingDiscovery.stop();
break;
}
console.log(`Fetched a Thing Description with title ${thingDescription.title}`);
counter++;
}
}
startDiscovery(); |
Noticing that the class semantics are a bit off in the example above, I tried again to use an interface ThingDescription {
title: string
}
class WoT {
discover (uri: string): ThingDiscovery {
return new ThingDiscovery(uri);
}
}
class ThingDiscovery implements AsyncIterable<ThingDescription> {
active: boolean = true;
private uri: string;
constructor(uri: string) {
this.uri = uri;
}
async* [Symbol.asyncIterator](): AsyncIterator<ThingDescription> {
while (this.active) {
const response = await fetch(this.uri);
const parsedTd = await response.json();
yield new Promise<ThingDescription>(resolve => resolve(parsedTd));
}
}
public stop() {
this.active = false;
}
}
// Just a random URI pointing to a TD
const uri = "https://raw.githubusercontent.com/w3c/wot-testing/main/events/2022.03.Online/TD/hitachi-esp-idf/TDs/hitachi-acc-air.td.jsonld";
async function startDiscovery() {
const wot = new WoT();
let thingDiscovery = wot.discover(uri);
let counter = 0;
for await (let thingDescription of thingDiscovery) {
if (counter === 3) {
thingDiscovery.stop();
break;
}
console.log(`Fetched a Thing Description with title ${thingDescription.title}`);
counter++;
}
}
startDiscovery(); |
Experimenting a bit more, I noticed that with the latest approach you can also access the iterator itself and call const iterator = thingDiscovery[Symbol.asyncIterator]();
const thingDescription = (await iterator.next()).value;
console.log(thingDescription); |
I created a new example which both implements a Edit: You can also try out the example directly in the Typescript playground using this link. Third Discovery Example in Typescriptinterface ThingDescription {
title: string
}
class WoT {
discover (uri: string): ThingDiscovery {
const thingDiscovery = new ThingDiscovery(uri);
thingDiscovery.start();
return thingDiscovery;
}
}
class ThingDiscovery implements AsyncIterable<ThingDescription> {
active: boolean = false;
private uri: string;
constructor(uri: string) {
this.uri = uri;
}
public async next() {
return await this[Symbol.asyncIterator]().next();
}
async* [Symbol.asyncIterator](): AsyncIterator<ThingDescription> {
if (!this.active) {
return;
}
const response = await fetch(this.uri);
const parsedTd = await response.json();
yield new Promise<ThingDescription>(resolve => resolve(parsedTd));
}
public stop() {
this.active = false;
}
public start() {
this.active = true;
}
}
// Just a random URI pointing to a TD
const uri = "https://raw.githubusercontent.com/w3c/wot-testing/main/events/2022.03.Online/TD/hitachi-esp-idf/TDs/hitachi-acc-air.td.jsonld";
async function startDiscovery() {
const wot = new WoT();
const thingDiscovery = wot.discover(uri);
for await (let thingDescription of thingDiscovery) {
console.log(`Fetched a Thing Description with title ${thingDescription.title}`);
}
// Alternative:
console.log(await thingDiscovery.next());
// Stop the discovery process
thingDiscovery.stop();
// Does not yield results, as the discovery process has been stopped.
for await (let thingDescription of thingDiscovery) {
console.log(`This will not be displayed`);
}
// Returns {"value": undefined, "done": true}
console.log(await thingDiscovery.next());
}
startDiscovery(); |
Thanks Jan. Minor nit: according to the spec, |
After the discussion we had in today's call I personally see the following interfaces as a possible way forward... partial namespace WOT {
ThingDiscovery discover();
};
interface ThingDiscovery {
// return TD directly
Promise<ThingDescription> direct(string url);
// return TDs via Iterator
DiscoveryIterator directory(string url, ...);
DiscoveryIterator any(string url, ...);
// more ?
};
interface DiscoveryIterator {
// decide which properties we actually need
readonly attribute boolean active;
readonly attribute boolean done;
readonly attribute Error? error;
readonly attribute ThingFilter? filter;
// actual iterator methods
undefined start();
undefined stop();
Promise<ThingDescription> next();
async iterable<ThingDescription>;
}; |
A few comments,
The corrected Web IDL for @danielpeintner 's suggestion: partial namespace WOT {
ThingDiscovery discover(); // we don't really need this, do we?
};
interface ThingDiscovery {
constructor();
Promise<ThingDescription> direct(USVString url, optional ThingFilter options);
DiscoveryResults directory(USVString url, optional ThingFilter options);
DiscoveryResults queryAll(optional ThingFilter options);
undefined stop();
// decide which properties we actually need
readonly attribute boolean active; // discovery has started
readonly attribute boolean done; // mapped to iterators' `done`
readonly attribute Error? error;
readonly attribute ThingFilter? filter;
};
interface DiscoveryResults {
async iterable<ThingDescription>;
}; |
(Edited). partial namespace WOT {
Promise<ThingDescription> requestThingDescription(USVString url, optional ThingFilter options);
};
interface ThingDiscovery {
constructor();
readonly attribute boolean active;
readonly attribute boolean done;
readonly attribute Error? error; // protocol errors
readonly attribute ThingFilter? filter;
// runtime errors (not protocol errors) are handled by Promise
Promise<undefined> queryDirectory(USVString url, optional ThingFilter options);
Promise<undefined> queryAll(optional ThingFilter options);
undefined stop();
async iterable<ThingDescription>;
}; Note that the An example of using this "combined" (control + iterator) interface, in current and new versions. let discovery = new ThingDiscovery({ method: "direct" });
for await (let td of discovery) {
console.log("Found Thing Description for " + td.title);
} let discovery = new ThingDiscovery();
discovery.queryAll().then( () => {
for await (let td of discovery) {
console.log("Found Thing Description for " + td.title);
}
// Check if iterator stopped since value not available because of an error.
if (discovery.error) {
console.log("There was a protocol error": + discovery.error.message);
}
}).catch(error) {
console.log("Could not start discovery: " error.message); // maybe security error
} |
Note that the proposal here (Daniel's modified) and here (my last) are almost the same, except
We can discuss these differences separately, but the starting point is the first proposal (modified from Daniel's). I think it's a good start. It would also allow something like the following: try {
for await (let td of wot.discover().queryAll()) {
console.log("Found Thing Description for " + td.title);
}
} catch(error) {
console.log("There was an error": + error.message);
} Which is pretty nice, minimal, and feasible (if we define properly how to throw). |
I read through this issue thread and also read the Discovery spec once again. I think we are getting closer to a consensus. Some doubts that I still have:
To solve 1. I would: partial namespace WOT {
Promise<ThingDescription> requestThingDescription(USVString url, optional ThingFilter options);
};
interface ThingDiscovery {
constructor();
// runtime errors (not protocol errors) are handled by Promise
Promise<ThingDiscoveryProcess> queryDirectory(USVString url, optional ThingFilter options);
Promise<ThingDiscoveryProcess> queryAll(optional ThingFilter options);
};
interface ThingDiscoveryProcess {
readonly attribute boolean active;
readonly attribute boolean done;
readonly attribute Error? error; // protocol errors
readonly attribute ThingFilter? filter;
undefined stop();
async iterable<ThingDescription>;
}
|
I like the latest approach. We need to think about how future use cases would change the API: local, bluetooth/nfc, multicast etc. I think they could be covered by the |
Scripting Call 2022-11-14
TODO / questions:
|
Just writing down what @relu91 proposed (with point 2) interface ThingDiscovery {
constructor();
// runtime errors (not protocol errors) are handled by Promise
// query() is manual (script controls the traversal),
// handles both fetching a single TD, and a TDD
Promise<ThingDiscoveryProcess> query(USVString url, optional QueryFilter options);
// queryAll() leaves all complexity to the implementation,
// and MAY specify URL filters as well
Promise<ThingDiscoveryProcess> queryAll(optional ExtendedQueryFilter options);
};
interface ThingDiscoveryProcess {
readonly attribute boolean active;
readonly attribute boolean done;
readonly attribute Error? error; // protocol errors
// readonly attribute ThingFilter? filter; // no longer needed
undefined stop();
async iterable<ThingDescription>;
}; An alternative would be to expose both |
I think we are converging :-) I was asking myself "what" should happen if query(...) gets called with an url of directory. Shall it return the ThingDescription of the Directory or (as in your case) the actual thing TDs behind the directory. I tend to agree with your thinking. Note: I think
👍 |
That is right. Removed. |
To record the current stage of discussion: partial namespace WOT {
// get a single TD, or fail
Promise<ThingDescription> requestThingDescription(USVString url);
// Manual traversal of TDD, but also handles single TD at URL
Promise<ThingDiscoveryProcess> discover(USVString url, optional ThingFilter options);
// discoverAll() leaves all control and complexity to the implementation,
// and MAY specify URL filters as well
Promise<ThingDiscoveryProcess> discoverAll(optional ExtendedThingFilter options);
};
interface ThingDiscoveryProcess {
readonly attribute boolean active;
readonly attribute boolean done;
readonly attribute Error? error; // protocol errors
undefined stop();
async iterable<ThingDescription>;
}; I think we can have a separate method to "get a single TD or else fail" - I have removed the ThingFilter options from it (not really needed, since one or zero TD is expected anyway). The Obviously |
The IDL above doesn't look much different from the current do-it-all I can imagine the following use cases:
|
Thank you for writing down the use cases. They fit more or less the ones that I had in mind in the last call. To use a more Discovery spec oriented terminology:
Also, I think use case 2 is supported by 1, right? Basically in mind, we should support:
Those cover the plausible scenarios in WoT application development:
|
These 2 could be supported by the
That is the What I am wondering about is the semantics of |
The Web IDL discussed in the call. partial namespace WOT {
// get a single TD, or fail
Promise<ThingDescription> requestThingDescription(USVString url);
// Manual or automatic traversal of TDD
Promise<ThingDiscoveryProcess> exploreDirectory(USVString url, optional ThingFilter options);
// discover() leaves all control and complexity to the implementation,
// and MAY specify URL filters as well
Promise<ThingDiscoveryProcess> discover(optional ExtendedThingFilter options);
};
interface ThingDiscoveryProcess {
readonly attribute boolean active;
readonly attribute boolean done;
readonly attribute Error? error; // protocol errors
undefined stop();
async iterable<ThingDescription>;
};
dictionary ThingFilter {
// USVString? query;
object? fragment;
};
dictionary ExtendedThingFilter extends ThingFilter {
USVString? url;
}; |
I am thinking to add a filter option whether to (recursively) follow links (of other TDDs) in a TDD and how deep, or stay at the top level. dictionary ThingFilter {
// USVString? query;
object? fragment;
unsigned short traversalDepth = 1; // follow directory links up to this depth, allowed 1-9?
// or have a different name and functionality?
// unsigned short? followLinks = 0; // extra depth to follow TD links, sensible allowed range: 0-9?
}; Anyway, this will be done later. |
In the call on 28.11.2022 we discussed how to expose local Things: via a filter, or via a separate method. |
Hi everyone :) I was dealing with implementing directory discovery in dart_wot lately and I was wondering a bit about how exactly the process should work here:
Sorry if the answer to these questions should be obvious, unfortunately, I haven't really been able to follow the discussions in the TF in detail for the last couple of months :/ In any case: Thank you! :) |
I think we could consider closing this issue (since we now have a new API in place) and maybe create a new issue from my comment above (#364 (comment)) that deals with describing the algorithm used for the |
Do we consider this addressed by #441 ? If yes, we can close and continue discussing the new points in a new issue. |
Call 11 Dec
|
When trying to implement the current discovery API I got the impression that its current design could be simplified by using a structure similar to the subscription/observation of events/properties. In particular, I had problems with implementing step two of the
next()
method:Correct me if I am wrong, but it seemed to me as if this would only be possible by continuously checking the results, using a function like
setTimeout
or within a while loop (are there any implementation examples?). As this didn't seem that optimal to me, I tried out a callback based approach instead (similar to theSubscription
interface and theobserveProperty
/subscribeEvent
methods) which worked quite well so far.I therefore wanted to suggest changing the signature of the
discover
method (or its potential successors which are discussed in #222) to something likewhere the
DiscoveryListener
function signature could look like the following:A minimal example could look like this:
Here, the discovery process is initialized using the
direct
method. When a Thing Description has been retrieved successfully, thehandleDiscovery
function is called which consumes the TD and interacts with the Thing. If I am not mistaken, a similar approach (using a class calledObservable
) was already part of the specification some time ago but I still wanted to open this issue to discuss it as an alternative to the currentdiscover
method.I am looking forward to your feedback!
The text was updated successfully, but these errors were encountered: