Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/zones.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { EnvironmentInjector, NgZone, runInInjectionContext } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ɵAngularFireSchedulers, ɵzoneWrap } from '@angular/fire';
import { Observable, Subject, asapScheduler } from 'rxjs';
import { AsapScheduler } from 'rxjs/internal/scheduler/AsapScheduler';
import { take } from 'rxjs/operators';

describe('ɵzoneWrap delivery timing', () => {

let source: Subject<number>;
let stream: Observable<number>;
let ngZone: NgZone;

beforeEach(() => {
source = new Subject<number>();
ngZone = TestBed.inject(NgZone);
const read = ɵzoneWrap(() => source.asObservable(), true);
stream = runInInjectionContext(TestBed.inject(EnvironmentInjector), () => ngZone.run(() => read()));
});

it('delivers an Observable value asynchronously, inside the Angular zone, before a zero-delay timer registered when the source emitted', async () => {
const delivery = await new Promise<{ delivered: boolean; afterEmit: boolean; inAngularZone: boolean }>(resolve => {
let emitted = false;
stream.pipe(take(1)).subscribe(() => resolve({ delivered: true, afterEmit: emitted, inAngularZone: NgZone.isInAngularZone() }));
ngZone.runOutsideAngular(() => {
// The timer races the delivery, so a dropped or late value fails here, not on the spec timeout.
setTimeout(() => resolve({ delivered: false, afterEmit: emitted, inAngularZone: false }), 0);
source.next(1);
emitted = true;
});
});
expect(delivery.delivered).withContext('delivered before the zero-delay timer').toBe(true);
expect(delivery.afterEmit).withContext('never synchronous').toBe(true);
expect(delivery.inAngularZone).withContext('delivered inside the Angular zone').toBe(true);
});

it('delivers through its own asap scheduler, not the one rxjs shares with every other caller', () => {
const delegate = Reflect.get(TestBed.inject(ɵAngularFireSchedulers).insideAngular, 'delegate');
expect(delegate).toBeInstanceOf(AsapScheduler);
expect(delegate).not.toBe(asapScheduler);
});

});
15 changes: 10 additions & 5 deletions src/zones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import {
SchedulerAction,
SchedulerLike,
Subscription,
asyncScheduler,
queueScheduler
} from 'rxjs';
import { AsapAction } from 'rxjs/internal/scheduler/AsapAction';
import { AsapScheduler } from 'rxjs/internal/scheduler/AsapScheduler';
import { observeOn, subscribeOn } from 'rxjs/operators';

declare const Zone: {current: unknown} | undefined;

const microtaskScheduler = new AsapScheduler(AsapAction);

export enum LogLevel {
"SILENT" = 0,
"WARN" = 1,
Expand Down Expand Up @@ -57,9 +60,11 @@ export class ɵZoneScheduler implements SchedulerLike {
}
};

// Scheduling itself needs to be run in zone to ensure setInterval calls for async scheduling are done
// inside the correct zone. This scheduler needs to schedule asynchronously always to ensure that
// firebase emissions are never synchronous. Specifying a delay causes issues with the queueScheduler delegate.
/* Delivery re-enters the target zone through runGuarded above, whatever zone the delegate's
* microtask or timer runs in. The delegate must always schedule asynchronously, so firebase
* emissions are never synchronous, and with the asap delegate a positive delay falls back to a
* timer, so callers must keep the delay at zero to keep microtask delivery. Specifying a delay
* causes issues with the queueScheduler delegate. */
return this.delegate.schedule(workInZone, delay, state);
}
}
Expand All @@ -79,7 +84,7 @@ export class ɵAngularFireSchedulers {
this.insideAngular = ngZone.run(
() => new ɵZoneScheduler(
typeof Zone === 'undefined' ? undefined : Zone.current,
asyncScheduler
microtaskScheduler
)
);
}
Expand Down
Loading