14

Suppose I structure my Angular app using observable data services, backed by serverside endpoints:

@Injectable()
export class TodoService {
  todos: Observable<Todo[]>
  private _todos: BehaviorSubject<Todo[]>;

  constructor(private http: Http) {
    this._todos = <BehaviorSubject<Todo[]>>new BehaviorSubject([]);
    this.todos = this._todos.asObservable();
  }

  loadData() {
    this.http.get(dataUrl).subscribe(data => this._todos.next(data));
  }
}

Now suppose my data has reference to some other model and exposed by some other service (some form of Many-to-Many relationship):

interface ToDo {
  title: string;
  status: StatusEnum;
  requiredResouces: Resource[];
}
interface Resource {
  name: string;
  todos: ToDo[];
}

@Injectable()
export class ResourcesService {
  resources: Observable<Resource[]>
  private _resources: BehaviorSubject<Resource[]>;

  ...
}

Now, suppose I add a method to either service that "links" a todo and a resource, that service will be able to push an updated state down the subject, but the other service will be unaware of the change. For example:

export class TodoService {

  ...

  addResourceRequirement(todo: ToDo, resource: Resource) {
    this.http.post(`${url}/${todo.id}/`, {addResource: resource.id})
      .subscribe(() => this.loadData());
  }
}

Would cause any "todos" observer to refresh, but any "resources" observer would still show the old state...

What design / pattern / architecture would you use to synchronize both services?


(I know there are architectures that avoid this difficulty as a whole - particularly flux based solutions - NgRX etc... but I'm interested in a solution specifically for the observable data services pattern)

Amit
  • 45,440
  • 9
  • 78
  • 110
  • Take a look at this resources http://blog.danieleghidoli.it/2016/10/22/http-rxjs-observables-angular/ and https://blog.angularindepth.com/learn-to-combine-rxjs-sequences-with-super-intuitive-interactive-diagrams-20fce8e6511 – Eduardo Vargas Apr 04 '18 at 08:41
  • @EduardoVargas - interesting links, and maybe some pattern based on the use cases listed there could be devised. Want to take a shot at answering? – Amit Apr 04 '18 at 09:01

2 Answers2

5

Your pattern is almost done, you just need to avoid creating the observable at boot time, also, keep in mind that BehaviorSubject extends Observable, so you can use it as it is, using an implicit getter if you want to get it easy to use.

@Injectable()
export class TodoService {
  private _todos: BehaviorSubject<Todo[]>;

  constructor() {
    this._todos = new BehaviorSubject<Todo[]>([]);
  }

  public get todos(): Observable<Todo[]>{
    return this._todos;
  }
}

Then, when you want to add data, simply emit a new value in your _todos subject, see RxJs: Incrementally push stream of data to BehaviorSubject<[]> for incremental array emission if you want to emit everything each time you add some data.

Edit

If a service has a dependency over todos data, you just have to build a new Observable using map, mergeMap, etc operators in order to build a new Observable with todos as source, allowing you to have a new value emitted if the root data changes.

Example:

@Injectable()
export class ResourcesService{

  private _resources: Observable<Resources[]>;

  constructor(private todoService: TodoService){
    this._resources = this.todoService.todos.map(todos => {
      // I suppose you want to aggreagate all resources, you can do it using reduce or anything, I'll use forEach in this example
      const resources: Resource[] = [];
      todos.forEach(todo => resources.push(...todo.requiredResouces);
    });
  }

  //And then implicit getter for _resources.

}

This way, if your TodoService emits a new array, ResourcesService will emit a new array of todos with status done, without needing any other operation.

Another approach

If your resources are coming from another endpoint (meaning that you need another request to fetch them after you updated your data) you might be better using a reloader pattern:

@Injectable()
export class DataReloaderService{

  public readonly reloader: BehaviorSubject<void> = new BehaviorSubject<void>(null);

  public reload():void{
    this.reloader.next(null);
  }
}

Then, whenever you create a data service, you just have to merge it with the reloader observable:

@Injectable()
export class ResourceService {

  private _resources: Observable<Resource[]>;

  constructor(private reloaderService: DataReloaderService, private http: HttpClient){
    this._resources = this.reloaderService.reloader.mergeMap(() => this.http.get(...));
  }

}

Finally, in your service doing modifications:

export class TodoService {

  private _todos: BehaviorSubject<Todo[]>;

  constructor(private reloaderService: DataReloaderService, private http: HttpClient) {
    this._todos = this.reloaderService.reloader.mergeMap(() => this.http.get(dataUrl));
  }

  public get todos(): Observable<Todo[]>{
    return this._todos;
  }

  addResourceRequirement(todo: ToDo, resource: Resource) {
    this.http.post(`${url}/${todo.id}/`, {addResource: resource.id})
      .subscribe(() => this.reloader.reload());
  }
}

NOTE: You should not subscribe in a service, the intent of Observables is to build a cold chain, where you just subscribe in the display part, this second pattern ensures that all your Observables are linked to the central reloader (you can also create multiple reloaders, one per model family for instance), subscription makes you loose that, resulting in strange ways to edit data only. if everything relies on your api, then your observables should only use this api, calling the reloader whenever you edit something to ensure all linked data are updated as well.

Supamiu
  • 8,501
  • 7
  • 42
  • 76
  • To be honest, I tried to re-read the question multiple times, to understand it properly, and it's one of the first times ever I don't see what's the issue with my answer, it's not my first answer here and to me it gives a solution to the problem. Downvoting the question wasn't childish at all, I did it because I can't understand your problem even after reading it 4 times, which is the description of a question with not enough details, isn't it? votes are not here to punish, they're here to show if a question is easy to understand or not, same for the answer, so you should downvote me. – Supamiu Apr 04 '18 at 08:46
  • I added further explanation using an example, maybe (hopefully?) it's clearer now. (p.s. to keep the question and answer in sync, I would suggest to avoid adding new data - `todosDone` is not part of the question) – Amit Apr 04 '18 at 09:00
  • @Amit Correct me if I'm wrong, but your issue is that your resource Observable won't be updated if the todo observables emit a new value? If it's the case, can you please add details on how you create your Resource observables? I think the problem is here. – Supamiu Apr 04 '18 at 09:04
  • @Supamiu - exactly the same way as the `ToDoService`. simple members, simple constructor that binds the subject and observable and some form of loading data (`loadData()` method for example). The point is that the server is responsible for the data and referential integrity - the client can always query for latest state, but shouldn't apply any business logic to the data. – Amit Apr 04 '18 at 09:08
  • @Amit edited my answer to add more details and another approach. – Supamiu Apr 04 '18 at 09:30
  • much better. I think the first part is still irrelevant and probably needs to be edited out to bring focus to the latter answer. (+1 :-) – Amit Apr 04 '18 at 09:44
1

I am not sure I understand the question.

But lets suppose you have both subjects with lists of resources and todos.

import {combineLatest} from "rxjs/observable/combineLatest";

mergeResourcesAndTodos() {
    return combineLatest(
        this.todoService.todos
        this.resourceService.resources
      ).map((data: any[]) => {
       const todos=data[0]
       const resources=data[1]
       //and here you can map anyway you want like a list of todos that have a list of resources or the other way around, here I will do an example with a list of todos with resources.
         return todos.map(todo=>{
              const resource= resources.find(resource=>resource.id===todo.resourceId)
              todo.resource=resource;
              return todo
          })
     })
  }
Eduardo Vargas
  • 8,752
  • 2
  • 20
  • 27
  • So the `forkJoin`ed observable would emit new data whenever either observable emits, but what would cause the source observable (for example `resources`) to emit new data after `todoService.addResourceRequirement(...)` is called? – Amit Apr 04 '18 at 09:14
  • It would emmit new data when either observables change (todo or resource). So if that function changes either of its respective behavior subject it would update the observable the function I created returns. – Eduardo Vargas Apr 04 '18 at 09:18
  • So this function is just to merge the Observables, if you wanna update it you should just change each behavior subject individually. – Eduardo Vargas Apr 04 '18 at 09:21
  • This wont work, as `forkJoin` will wait for the source observables to complete before emitting, what will not happen unless OP explicitely invokes `subject.complete()` in the services. – Jota.Toledo Apr 04 '18 at 09:30
  • try with combineLatest then – Eduardo Vargas Apr 04 '18 at 09:32