opral / inlang-sdk

0 stars 0 forks source link

Reactive project.plugins and project.settings #120

Closed samuelstroschein closed 1 month ago

samuelstroschein commented 2 months ago

Context

State like project.plugins depends on project.settings. We have different ways to model the dependency. I am unsure which one is the way to go.

Options

  1. Higher order observables https://rxjs.dev/guide/operators#higher-order-observables
const settings$ = new BehaviorSubject(settings)
const plugins$ = settings$.pipe(
  // do something
)
  1. subscribe and trigger side effects
const settings$ = new BehaviorSubject(settings)
const plugins$ = new BehaviorSubject(initialLoadPlugins)

settings$.subscribe((newSettings) => {
  if (newSettings.modules !== oldSettings.modules){
    // re-import the plugins (can be optimized later for individual plugins etc)
    plugins$.next(await importPlugins())
  }
})
  1. ???
samuelstroschein commented 2 months ago

@jan.johannes @martin.lysk1 input desired

samuelstroschein commented 2 months ago

@jan.johannes i suspect we will choose the same pattern we decide on here in the lix sdk

samuelstroschein commented 2 months ago

Option 3, if plugins depends on settings, we can also model that in setSettings

samuelstroschein commented 2 months ago

Updates after 2 hours of banging my head against the wall:

Observables can't be used for state as they don't hold their last value. Correctly modeling reactive state entails the avoidance of re-triggers. That goes against Observables which are modeled as streams (of events).

Which means that BehaviourSubject must be used. BehaviorSubject however can't model derived state. Hence, implicit dependent state (option 2 and 3) is the only option with RxJS. That will surely lead to chaos down the road. Someone forgot that X triggers Y, or Y is not awaited as a result of X, etc.

I am going with approach number 3 to solve this ticket but I am confident in the long term viability. Ideally, we have a state management solution that has a first class concept of derived state.

samuelstroschein commented 2 months ago

As an example of explicit derived state: I wouldn't know how to model project.errors with an implicit model.

Declarative derived state

const errors$ = merge(
    pluginErrors$,
    userErrors$
    otherErrors$
)

Implicit derived state

const errors$ = new BehaviorSubject([]);

    pluginsErrors$.subscribe((errors) => {
        errors$.next([...errors, ...$userErrors.getValue(), ...$otherErrors.getValue()]);
    }

    userErrors$.subscribe((errors) => {
        errors$.next([...errors, ...$pluginErrors.getValue(), ...$otherErrors.getValue()]);
    }

    otherErrors$.subscribe((errors) => {
        errors$.next([...errors, ...$pluginErrors.getValue(), ...$userErrors.getValue()]);
    }
samuelstroschein commented 2 months ago

Edit: Ugh no it's just share. Now the shareReplay makes sense.

I did find a solution to RxJS observables that should save their last state. shareReplay is what we are looking for https://rxjs.dev/api/index/function/shareReplay.

Off-topic the jargon that RxJS uses is … unintuitive. shareReplay, really? Why not call it shareLastValue. What's the replay supposed to indicate?

Another one is distinctUntilChanged. In code it reads more like a onlyTriggerIfChanged though. Next to distinct, distinctUntilChangedKey and what not. Huge API. For unknown reasons distinctUntilChanges isn't even triggered. Cool.

CleanShot 2024-08-27 at 20.37.28@2x.png

samuelstroschein commented 2 months ago

Did some more research. If awaiting side-effects is important, a state solution needs a concept of "computing". RxJS (and Effector) emits event A with no knowledge if event B is about to happen.

The knowledge that event B is about to happen can be used to "await" event B. RxJS is getting close with the concept of a "completed" observable and the await takeLastValue function. Unfortunately, complete kills the subscriptions. Hence, awaiting a side effect is possible but only once.

samuelstroschein commented 2 months ago

@jan.johannes can you elaborate or we can sync quickly in around?