pod-os / PodOS

Personal Online Data Operating System
MIT License
14 stars 1 forks source link

Reactivity to state changes #37

Open josephguillaume opened 5 months ago

josephguillaume commented 5 months ago

Opening this issue to document opportunities for components to react to state changes, building on the rxjs decision:

https://github.com/pod-os/PodOS/blob/7d2693a3b47cc3bc837060bc761d5aabf42c2b27/docs/decisions/0009-introduce-rxjs.md?plain=1#L19

By state changes, I firstly have in mind cases where the triples affecting a subject or object change, either because of another component on the same page or due to data changes. The primary use case is to perform reactive rendering. It will likely be desirable to standardise how this is dealt with across solid web components. See https://github.com/solid-contrib/web-components/wiki/Rendering

e.g. pos-label is displaying data from the triple

:subject rdfs:label "subject label".

This triple is then replaced in the store by

:subject rdfs:label "new subject label".

pos-label needs to rerender with the new value

josephguillaume commented 5 months ago

Because PodOs uses rdflib.js, at the moment it is possible for custom components to achieve reactive rendering using callbacks on the graph store.

os.store.graph.addDataCallback(doUiCallbacks);
os.store.graph.rdfArrayRemove = (a, x) => {
    for (var i = 0; i < a.length; i++) {
      if (
        a[i].subject.equals(x.subject) &&
        a[i].predicate.equals(x.predicate) &&
        a[i].object.equals(x.object) &&
        a[i].why.equals(x.why)
      ) {
        doUiCallbacks(x);
        a.splice(i, 1);
        return;
      }
    }
    throw new Error("RDFArrayRemove: Array did not contain " + x + " " + x.why);
  };

doUiCallbacks is called on every triple change. It therefore needs to efficiently manage a set of callbacks to individual custom components as well as throttle repeat callbacks in some way.

I have a basic buggy implementation of this.

josephguillaume commented 5 months ago

Given the decision to use rxjs, it makes sense to revisit this callback architecture and identify a solution better suited to PodOs.

PodOs core encapsulates rdflib.js so that elements do not depend on it ^1

Building on the idea that rxjs provides lazy push collections of multiple values, a possible solution is that the properties of Thing ^2 become observables

e.g. label is no longer a pull function that returns a value, but is instead a push observable that pos-label reacts to. The construction/destruction of a Thing would need to handle subscription to observables, and manage the callbacks from rdflib.js

This would be a major architectural change, which could either be approached as a breaking change, or by PodOS core offering both push and pull architectures for accessing data.

However, for devx reasons, I believe PodOs elements should either all be individually reactive to state or not, and I currently believe that making every web component state-reactive is the preferred option.

Whatever final architecture is used, I have found that retaining low level access to rdflib.js is often needed given PodOs does not fully cover the rdflib.js api. It would therefore be beneficial for the callback management mechanism to be accessible from custom components, even if direct use is discouraged.

There may be other solutions, which is why I've framed this issue as documenting opportunities rather than proposing this as the definitive solution.

josephguillaume commented 4 months ago

As a point of comparison, semantic-ko used observables through the knockoutjs library, though I think PodOS would want to avoid defining an explicit view model.

https://web.archive.org/web/20111118042156/http://antoniogarrote.com/semantic_ko/

josephguillaume commented 2 months ago

Noting that while external components will use rxjs, reactivity to logged in state (and Webid, and profile) is still managed using a stenciljs store for image, navigation bar, resource and document, with pos-app providing the bridge between rxjs and the store.

https://github.com/pod-os/PodOS/blob/7d2693a3b47cc3bc837060bc761d5aabf42c2b27/elements/src/store/session.ts

https://github.com/pod-os/PodOS/blob/7d2693a3b47cc3bc837060bc761d5aabf42c2b27/elements/src/components/pos-app/pos-app.tsx#L48

josephguillaume commented 4 days ago

If I understand correctly, here's an untested proof of concept for an observable rxjs label() used as part of https://github.com/pod-os/PodOS/blob/main/elements/src/components/pos-label/pos-label.tsx

@State() label = null;

receiveResource = (resource: Thing) => {
    this.resource = resource;
    this.resource.label()
      .pipe(takeUntil(this.disconnected$))
      .subscribe(label=>this.label=label)
};

render() {
    return this.label
}

However, we also need to deal with destruction of the component similar to https://github.com/pod-os/PodOS/blob/27226761cee3ead7fd6096e45d2177d7bfc330b8/contacts/src/components/open-address-book/open-address-book.tsx#L23-L33

@State() label = null;

receiveResource = (resource: Thing) => {
    this.resource = resource;
    this.resource.label()
      .pipe(takeUntil(this.disconnected$))
      .subscribe(label=>this.label=label)
  };

render() {
    return this.label
}

private readonly disconnected$ = new Subject<void>();

disconnectedCallback() {
    this.disconnected$.next();
    this.disconnected$.unsubscribe();
}

This seems a bit unwieldy and probably would need to be encapsulated somehow?

josephguillaume commented 4 days ago

A common framework-specific solution is to proxy object properties to perform dependency-tracking and change-notification when properties are accessed or modified ^1

Using stencil-store, we could create a ReactiveThing in PodOS elements, which turns an observable label() into a reactive property, something like (untested):

class ReactiveThing {
  constructor(thing){
   this.disconnected$ = new Subject();
   this.state = createStore({
     label: null
   });
   thing.label()
      .pipe(takeUntil(this.disconnected$))
      .subscribe(label=>this.state.label=label)
  };
  dispose(){
    this.disconnected$.next();
    this.disconnected$.unsubscribe();
  }
}

Which would be used:

receiveResource = (resource: Thing) => {
    this.resource = new ReactiveThing(resource);
  };

render() {
    return this.resource.state.label
}

disconnectedCallback() {
    this.resource.dispose()
}
josephguillaume commented 4 days ago

SolidOS uses rdflib.js' store.updater.addDownstreamChangeListener. When the remote resource is changed a notification is received via Websocket, the resource is reloaded and a rerender is triggered.

https://github.com/search?q=org%3ASolidOS%20addDownstreamChangeListener&type=code

josephguillaume commented 4 days ago

Thing's interface is reminiscent of https://ldo.js.org

It uses a subscribable RDF dataset that emits events for specific quad matches, which triggers rerender of the react component, apparently by updating a forceUpdateCounter state variable.

useSubject appears to be an equivalent of Thing/ReactiveThing https://ldo.js.org/api/solid-react/useSubject/