Skip to content
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

Add qx.ui.window.NativeWindow with inter-window messaging #9780

Open
cboulanger opened this issue Aug 23, 2019 · 14 comments

Comments

@cboulanger
Copy link
Contributor

commented Aug 23, 2019

I think qooxdoo needs a better way of dealing with native browser windows (popups). One way this could be implemented is to create a special class qx.ui.window.NativeWindow that should have a common interface with qx.ui.window.Window on one hand, and qx.ui.embed.Iframe, on the other. It needs to provide for all the added functionality and the challenges of native browser windows. One of the biggest problems is that the windows have to load a separately compiled application, which introduces latency, and that communication with that window is limited. The window.postMessage API is simple and elegant, but not very "qooxdoo"-ish and requires some boilerplate.

I propose a communication channel that works with synchronizing application property state, so that data binding works across windows, i.e. so that something like this is possible:

// class used as application for popup window 
qx.Class.define("my.Application", {
  extend: qx.application.Standalone,
  include: qx.ui.window.MNativeWindow,
  properties: { foo: {check: "String"}}
// class used in main application
qx.Class.define("my.nativeWindow",
  extend: qx.ui.window.NativeWindow,
  properties: { foo: {check: "String"}}
});

const win = new my.nativeWindow(url, name, config);
this.bind("myFoo", win, "foo");
win.bind("foo", this, "myFoo"); 
win.synchronize({"foo":true}); // or "forward()"

The properties will be synchronized transparently by the qx.ui.window.MNativeWindow mixin included in the application in the popup and by the synchronize method of the qx.ui.window.NativeWindow class. Since the mechanism will use postMessage under the hub, main application and popup can be served from different origins.

In addition, we could add a way to selectively forward events between the remote app and the local window object.

Thoughts?

@derrell

This comment has been minimized.

Copy link
Member

commented Aug 25, 2019

Sounds like a great idea to abstract that communication!

@derrell

This comment has been minimized.

Copy link
Member

commented Aug 25, 2019

There are some timing issues. The native window or iframe app has to be loaded before you can do any communication. It requires checking that the window object of the native window or iframe exists yet, and then testing for something in it (such as window && window.qx && window.qx.isFinishedLoading() to ascertain that the window code is ready to communicate.

@cboulanger

This comment has been minimized.

Copy link
Contributor Author

commented Aug 25, 2019

You're right and this point made me realize that really three different forms of native windows have to be distinguished:

  1. a qx.ui.window.Html (the name can be improved) which shares most of its API with qx.ui.embed.Iframe: this is a widget where one wants to access the DOM, and thus API methods have to be provided which signal that this DOM is fully loaded. This window would normally have to have a shared origin or be CORS-compliant.

  2. a qx.ui.window.NativeWindow which would contain normal UI elements that are part of the current application, and which would have access to this application. This should be almost 1:1 API-equivalent to qx.ui.window.Window, and building an UI inside this widget should work exactly like in a qooxdoo window - maybe this could be made completely transparent by overloading the add() method. This window would not have an URL since the DOM would be constructed programmatically. Caveat: Extending a qooxdoo app into a native window this way is not yet technically possible because of issues with event registration (see #9772), but these could be solved.

  3. a qx.ui.window.Application which would load a complete qooxdoo app into the window. In this peer-to-peer situation, the app that creates an instance of qx.ui.window.Application should not concern itself with latency issues at all (except on the level of the business logic). Communication would work through the (eventual) sychronization of the application property state. You could set up all bindings to the properties of the qx.ui.window.Application, the class would take care of propagating property value changes back and forth. Of course, there are race conditions etc. to be considered, but they are separate from the loading of the app.

In this issue, I am only interested in (3), but (1) and (2) should probably always taken into account.

@johnspackman

This comment has been minimized.

Copy link
Member

commented Aug 25, 2019

I've been thinking along similar lines recently - doesn't this sound a lot like client-server communication? You could view it that the transport is a postmessage-based protocol as opposed to Xhr (the transport element could derive from qx.io.request.AbstractRequest) and then that the exchange of properties and triggering of events is a higher level protocol.

As a step on from that 😁, does it have to be just one, specific object that exchanges properties and events? If you could make it so that arbitrary objects could be synchronised, then you have a rich API for any kind of client-server synchronisation.

@cboulanger

This comment has been minimized.

Copy link
Contributor Author

commented Aug 25, 2019

@johnspackman This would be very cool indeed and I have thought about "databinding over the wire" for a long time[1][2]. It's an extremely interesting subject that would need some more thought than just the present case. But you're right that it solves basically the same problems. Maybe this could be a start. You need a protocol that initializes the connection, exchanges the initial state and sets up the synchronization of state changes on both sides. Windo-to-window communication can use postMessage, app-to-app communication would just need a different transport mechanism (such as JSONRPC) and, making it somewhat more complex, a way to uniquely address objects inside the applications.

[1] http://qooxdoo.678.n2.nabble.com/databinding-rpc-REST-resources-td6655875.html
[2] http://qooxdoo.678.n2.nabble.com/qooxdoo-contrib-cometd-Dialog-and-VirtualData-td4247446.html

@johnspackman

This comment has been minimized.

Copy link
Member

commented Aug 25, 2019

are/were you panyasan?

If you can de/serialise an object into a a neutral form (eg JSON) then it can be transmitted; and by monitoring events it could be incrementally serialised to minimise updates. This depends on a concept of object identity, which in turn raises issues for garbage collection - i.e for changes on Peer#1 to be updated on Peer#2, Peer#2 has to be able to always find the object via some kind of universal ID lookup .. and then if Peer#1 garbage disposes an object, how is this disposal passed to Peer#2?

I think that if you can solve the identity problem, you can formulate a strategy for the garbage problem, and then you have the basis of rich client-server object communication/synchronisation.

Also, if you're careful about the details of the protocol, it would be possible to make this the basis for a REST api, where the API is readable and documentable and could be implemented by anyone in any language.

So... I have some proof of concept work of this you might like; the remote communication part is not done yet, but I have a de/serialisation mechanism which solves the identity problem. The task is to be able to take a Qooxdoo object and serialise it into JSON for storage in a database; the mechanism must also support referenced objects and arrays of objects (via Qooxdoo object properties), and because there is the concept of identity you can serialise many-to-one and recursive object structures. As this supports references, it has to be able to locate existing, already deserialiased objects on demand - this means having a finite list of objects (the garbage collection problem).

I just pushed https://github.com/johnspackman/qxl.cms, it has working unit tests. Try test/TestDatabase.js. There's no documentation (except code comments) but the principals are that:

  • the properties which need to be synchronised are marked by using annotations eg qxl.cms.test.content.DemoContentPiece marks properties with qxl.cms.data.io.anno.Property.DEFAULT

  • qxl.cms.data.io.Controller manages all objects that need to be serialised; it keeps a universal list (the source of garbage collection problems) of all objects by UUID

  • qxl.cms.data.io.Object - base class for objects that can be persisted (it's optional, you can just implement qxl.cms.data.io.IObject and roll your own)

  • qxl.cms.data.io.ClassIo and qxl.cms.data.io.ClassRefIo implement de/serialisation of objects and references to objects

The annotations are currently setup to dictate what is de/serialised for a database - but that'll be abstracted slightly "real soon now" so there annotations for database and separate annotations for client-server/peer-to-peer serialisation.

qxl.cms.data.io.Controller is based around get/put logic suited to database serialisation, but IMHO the principal would also apply to client-server; Controller defers to an instance of qxl.cms.data.io.IDataSource to do the actual serialisation, so some abstraction is already under way.

I think database serialisation has the same challenges as remote I/O - what's not solved is remote method calls, but that should be pretty trivial.

FWIW qooxdoo-serverobjects (which I use a lot) suffers from a protocol which is difficult to document and is not REST friendly; it's almost binary in that way, and a nested protocol also makes it difficult to secure (you have to be quite high level). However it does bi-directional synchronisation of properties, and when it copies a property value that will trigger a property change event. It also has remote methods (one way, the client can call a server method).

@cboulanger

This comment has been minimized.

Copy link
Contributor Author

commented Aug 25, 2019

This would really deserve its own issue ("over-the-wire databinding")...

@johnspackman

This comment has been minimized.

Copy link
Member

commented Aug 25, 2019

or some combination of "automatic rest" and "remote objects" (which was where I was going with qso but ran out of imagination 😊).

It's interesting that this boils down to identity and serialisation. Everything else is part of Qooxdoo already, like properties. I also like annotations for exactly this type of purpose - meta data of code which does not require changes to the structure of the code.

@cboulanger

This comment has been minimized.

Copy link
Contributor Author

commented Aug 26, 2019

@johnspackman See #9781 - maybe you want to add the relevant pieces of your posts in this issue there.

@cboulanger

This comment has been minimized.

Copy link
Contributor Author

commented Aug 26, 2019

@johnspackman I wonder: do we really need to synchronize events beyond change events? Not that it is particularly difficult to implement on top of what needs to be done for databinding, but I wonder if databinding wouldn't be enough ("Keep it simple") - instead of dispatching an event, a property can be set to a certain value, which in turn dispatches the changeEvent. And this would allow to keep everything within the semantics of databinding.

@johnspackman

This comment has been minimized.

Copy link
Member

commented Aug 26, 2019

Yes, I agree. I did write a kind of event handling in server-objects but ended up only ever using it on the server side (and it's not straightforward). Just being able to copy properties and have the loading of properties on the other side trigger change events is enough.

If you need to make something happen on the other side, you call a remote method.

@cboulanger

This comment has been minimized.

Copy link
Contributor Author

commented Aug 27, 2019

Here is a first implementation / proof of concept, not in a fork of qooxdoo but in the event recorder namespace:
https://github.com/cboulanger/eventrecorder/tree/master/source/class/cboulanger/eventrecorder/window

Demo is here:
https://cboulanger.github.io/eventrecorder/remote_binding_test/

The current implementation is not based on a generalizable framework such as proposed in #9781 , but relies on a sort of mini-protocol, which is, however, completely sufficient for the task at hand.

@cboulanger

This comment has been minimized.

Copy link
Contributor Author

commented Aug 28, 2019

The demo now has support for synchronizing qx.data.Object and qx.data.Array objects - the list data which you see in opened windows is bound from the list in the main window. In fact, any qooxdoo object can in theory be serialized, but it doesn't work of course for those which need initialization parameters.

Next up: implementing qx.data.Array's changeBubble and change events - then we'll have true databinding across native windows!!

@cboulanger

This comment has been minimized.

Copy link
Contributor Author

commented Aug 29, 2019

We now have a fully functional prototype, including support for changeBubble events: https://cboulanger.github.io/eventrecorder/remote_binding_test/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.