Skip to content

Commit

Permalink
BREAKING data.model is synced with Apollo cache #9844
Browse files Browse the repository at this point in the history
`NaturalAbstractModelService.resolve()` now resolves in two steps. First
it asks Apollo to fetch the object from network to force refresh Apollo
cache. Then only, the resolving is considered finished, and we emit an
observable that watches Apollo cache for the model. So the component can
receive the observable from the route, subscribe to it and immediately
receive an up-to-date model. And all subsequent updates to the model
will be emitted from Apollo cache too.

But the real breaking part is that previously the route resolved data
shape:

```ts
{
    permission: myPermissions,
    action: {
        model: myAction,
        statuses: myStatuses,
        priorities: myPriorities,
    }
}
```

Now, because `NaturalAbstractModelService.resolve()` only resolve the
model, it becomes the simpler:

```ts
{
    permission: myPermissions,
    model: myActionObservable,
    statuses: myStatuses,
    priorities: myPriorities,
}
```

So all overrides of `resolve()` must be migrate outside the model
service. Typically, something like:

```ts
export const actionResolvers: ResolveData = {
    action: resolveAction,
    statuses: () => inject(NaturalEnumService).get('ActionStatus'),
    priorities: () => inject(NaturalEnumService).get('Priority'),
}
```
  • Loading branch information
PowerKiKi committed Nov 1, 2023
1 parent 0c4e0dd commit 017a5b1
Show file tree
Hide file tree
Showing 9 changed files with 61 additions and 441 deletions.
9 changes: 0 additions & 9 deletions projects/natural/src/lib/classes/abstract-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ export class NaturalAbstractDetail<
}

// Subscribe to model to know when Apollo cache is changed, so we can reflect it into `data.model`
let firstTime = true;
this.#modelSub?.unsubscribe();
this.#modelSub = incomingData.model
.pipe(takeUntil(this.ngUnsubscribe))
Expand All @@ -102,16 +101,8 @@ export class NaturalAbstractDetail<
...incomingData,
model: model,
} as Data<TService, ExtraResolve>;
// Initialize the form exactly once, and never again when the model is updated in Apollo cache
if (firstTime) {
firstTime = false;
console.log('firstTime', this.data.model);
}
});
console.log('initForm', this.data.model);
this.initForm();

console.log('FINISHED');
});
} else {
this.initForm();
Expand Down
259 changes: 2 additions & 257 deletions projects/natural/src/lib/classes/rxjs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {cancellableTimeout, debug, emptyResult, spyObservable, waitUntilFirstEmission} from './rxjs';
import {first, Observable, of, ReplaySubject, Subject} from 'rxjs';
import {cancellableTimeout} from './rxjs';
import {ReplaySubject, Subject} from 'rxjs';
import {fakeAsync, tick} from '@angular/core/testing';

describe('cancellableTimeout', () => {
Expand Down Expand Up @@ -65,258 +65,3 @@ describe('cancellableTimeout', () => {
expect(completed).toBe(true);
}));
});

describe('waitUntilFirstEmission', () => {
let source$: Observable<number>;
let sourceSubject: Subject<1 | 2 | 3>;

beforeEach(() => {
console.error('______________________________');
sourceSubject = new Subject<1 | 2 | 3>();
source$ = sourceSubject.pipe(); // For debugging it's convenient to drop `debug('source)` in there
});

it('observable is cold, nothing happen without subscription', () => {
const source = spyObservable(source$);
waitUntilFirstEmission(source.observable);

sourceSubject.next(1);
sourceSubject.next(2);
sourceSubject.next(3);

expect(source.result).toEqual(emptyResult);
});

it('first subscriber will receive exactly 1 emission and be completed automatically, but source is still subscribed', () => {
const source = spyObservable(source$);
const obs = waitUntilFirstEmission(source.observable);
const firstSubscriber = spyObservable(obs);

firstSubscriber.observable.subscribe();

sourceSubject.next(1);
sourceSubject.next(2);
sourceSubject.next(3);

expect(source.result).toEqual({
called: 3,
completed: 0,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});

expect(firstSubscriber.result).toEqual({
called: 1,
completed: 1,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});
});

fit('after first subscriber completes, second subscriber will receive first emission (replayed) and all others', done => {
// of(1,2,3,4).pipe(debug('QQQQQQQQQQQ'), first(),debug('QQQ'),).subscribe();
const source = spyObservable(source$.pipe(debug('source')));
const obs = waitUntilFirstEmission(source.observable.pipe(debug('resolver'), first()));
const firstSubscriber = spyObservable(obs);

firstSubscriber.observable.subscribe({
next: model => {
const secondSubscriber = spyObservable(model);
secondSubscriber.observable.pipe(debug('component')).subscribe({
next: value => {
switch (secondSubscriber.result.called) {
case 1:
expect(value).toBe(1);
break;
case 2:
expect(value).toBe(2);
sourceSubject.next(3);
break;
case 3:
expect(value).toBe(3);
sourceSubject.complete();
break;
default:
throw new Error(`should not be called ${secondSubscriber.result.called} times`);
}
},
complete: () => {
expect(source.result)
.withContext('when second subscriber is completed, that means our source was completed')
.toEqual({
called: 3,
completed: 1,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});

expect(secondSubscriber.result)
.withContext('second subscriber is completed and it received all emission from source')
.toEqual({
called: 3,
completed: 1,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});

done();
},
});

expect(secondSubscriber.result)
.withContext('second subscriber immediately receives the first emission as a replay')
.toEqual({
called: 1,
completed: 0,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});
},
});

sourceSubject.next(1);
sourceSubject.next(2);

expect(firstSubscriber.result).toEqual({
called: 1,
completed: 1,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});
});

it('idem but with `of()`', done => {
const source = spyObservable(of(1, 2, 3).pipe(debug('source')));
const obs = waitUntilFirstEmission(source.observable.pipe(debug('resolver'), first()));
const firstSubscriber = spyObservable(obs);

firstSubscriber.observable.subscribe({
next: model => {
const secondSubscriber = spyObservable(model);
secondSubscriber.observable.pipe(debug('component')).subscribe({
next: value => {
switch (secondSubscriber.result.called) {
case 1:
expect(value).toBe(1);
break;
default:
throw new Error(`should not be called ${secondSubscriber.result.called} times`);
}
},
complete: () => {
expect(source.result)
.withContext('when second subscriber is completed, that means our source was completed')
.toEqual({
called: 1,
completed: 0,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});

expect(secondSubscriber.result)
.withContext('second subscriber is completed and it received all emission from source')
.toEqual({
called: 1,
completed: 1,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});
console.log('SUCESS');
done();
},
});

expect(secondSubscriber.result)
.withContext('second subscriber immediately receives the first emission as a replay')
.toEqual({
called: 1,
completed: 0,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});
},
});

expect(firstSubscriber.result).toEqual({
called: 1,
completed: 1,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});
});

it('if first subscriber unsubscribes (before first emission), source is unsubscribed', () => {
const source = spyObservable(source$);
const obs = waitUntilFirstEmission(source.observable);
const firstSubscriber = spyObservable(obs);

const subscription = firstSubscriber.observable.subscribe();
subscription.unsubscribe();

expect(source.result).toEqual({
called: 0,
completed: 0,
errored: 0,
subscribed: 1,
unsubscribed: 1,
});

expect(firstSubscriber.result).toEqual({
called: 0,
completed: 0,
errored: 0,
subscribed: 1,
unsubscribed: 1,
});
});

it('if second subscriber unsubscribes, source is unsubscribed', fakeAsync(() => {
const source = spyObservable(source$);
const obs = waitUntilFirstEmission(source.observable);
const firstSubscriber = spyObservable(obs);

firstSubscriber.observable.subscribe({
next: model => {
const secondSubscriber = spyObservable(model);
const subscription = secondSubscriber.observable.subscribe();
subscription.unsubscribe();

expect(secondSubscriber.result).toEqual({
called: 1,
completed: 0,
errored: 0,
subscribed: 1,
unsubscribed: 1,
});
},
});

sourceSubject.next(1);

expect(source.result).toEqual({
called: 1,
completed: 0,
errored: 0,
subscribed: 1,
unsubscribed: 1,
});

expect(firstSubscriber.result).toEqual({
called: 1,
completed: 1,
errored: 0,
subscribed: 1,
unsubscribed: 0,
});
}));
});
Loading

0 comments on commit 017a5b1

Please sign in to comment.