Skip to content

Commit 07ec036

Browse files
committed
fix(offline): order connectivity probe observations
1 parent 22b47b8 commit 07ec036

3 files changed

Lines changed: 25 additions & 6 deletions

File tree

projects/kit/offline/src/lib/offline-network-verification.spec.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,30 @@ describe('OfflineNetworkService connection verification', () => {
5959
expect(service.checkingConnection()).toBe(false);
6060
});
6161

62-
it('leaves a newer successful observation intact after a status-zero failure and permits retry', async () => {
62+
it('records a status-zero failure when no newer API observation exists and permits retry', async () => {
6363
const markApiFailure = vi.spyOn(service, 'markApiFailure');
6464
const first = service.verifyConnection('/status');
65-
service.markApiSuccess();
6665
http.expectOne('/status').error(new ProgressEvent('error'));
6766
await expect(first).resolves.toBe(false);
68-
expect(markApiFailure).not.toHaveBeenCalled();
69-
expect(service.state()).toBe('unverified');
67+
expect(markApiFailure).toHaveBeenCalledOnce();
68+
expect(service.state()).toBe('offline');
7069

7170
const second = service.verifyConnection('/status');
7271
http.expectOne('/status').flush({});
7372
await expect(second).resolves.toBe(true);
7473
});
7574

75+
it('does not let an older probe failure overwrite a newer successful API observation', async () => {
76+
const markApiFailure = vi.spyOn(service, 'markApiFailure');
77+
const verification = service.verifyConnection('/status');
78+
service.markApiSuccess();
79+
http.expectOne('/status').error(new ProgressEvent('error'));
80+
81+
await expect(verification).resolves.toBe(false);
82+
expect(markApiFailure).not.toHaveBeenCalled();
83+
expect(service.state()).toBe('unverified');
84+
});
85+
7686
it('does not overwrite reachability when a stalled check times out and permits retry', async () => {
7787
vi.useFakeTimers();
7888
const markApiFailure = vi.spyOn(service, 'markApiFailure');

projects/kit/offline/src/lib/offline-network.service.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class OfflineNetworkService {
2222
readonly #apiReachable = signal<boolean | null>(null);
2323
readonly #appActive = signal(true);
2424
readonly #lifecycleRevision = signal(0);
25+
#apiReachabilityRevision = 0;
2526
#networkRevision = 0;
2627
readonly #listeners: PluginListenerHandle[] = [];
2728
#initialized = false;
@@ -67,10 +68,12 @@ export class OfflineNetworkService {
6768
}
6869

6970
markApiSuccess(): void {
71+
this.#apiReachabilityRevision += 1;
7072
this.#apiReachable.set(true);
7173
}
7274

7375
markApiFailure(): void {
76+
this.#apiReachabilityRevision += 1;
7477
this.#apiReachable.set(false);
7578
}
7679

@@ -81,6 +84,7 @@ export class OfflineNetworkService {
8184
verifyConnection(url: string, timeoutMs = DEFAULT_OFFLINE_CONNECTION_VERIFICATION_TIMEOUT_MS): Promise<boolean> {
8285
if (this.#connectionVerification) return this.#connectionVerification;
8386

87+
const startingApiReachabilityRevision = this.#apiReachabilityRevision;
8488
this.#checkingConnection.set(true);
8589
const verification = firstValueFrom(
8690
this.#http
@@ -94,7 +98,12 @@ export class OfflineNetworkService {
9498
this.markApiSuccess();
9599
return true;
96100
}),
97-
catchError(() => of(false)),
101+
catchError((error: unknown) => {
102+
if (isOfflineFallbackError(error) && this.#apiReachabilityRevision === startingApiReachabilityRevision) {
103+
this.markApiFailure();
104+
}
105+
return of(false);
106+
}),
98107
),
99108
).finally(() => {
100109
this.#checkingConnection.set(false);

projects/kit/offline/src/lib/offline-request-policy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { HttpContextToken } from '@angular/common/http';
55

66
/** outbox再送時にoffline interceptorだけを迂回する。認証・retryは維持する。 */
77
export const OFFLINE_BYPASS = new HttpContextToken<boolean>(() => false);
8-
/** Keeps a manual reachability probe from overwriting a newer successful API observation. */
8+
/** Lets a manual reachability probe apply its transport failure with its own observation-order guard. */
99
export const OFFLINE_IGNORE_TRANSPORT_FAILURE = new HttpContextToken<boolean>(() => false);
1010
/** Header attached to synthetic local or optimistic responses. */
1111
export const OFFLINE_RESPONSE_HEADER = 'X-Offline-Response';

0 commit comments

Comments
 (0)