Skip to content
Merged
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
20 changes: 18 additions & 2 deletions projects/kit/src/lib/realtime/kit-realtime-connection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { PluginListenerHandle } from '@capacitor/core';
import { Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { kitRealtimeProtocols, KitRealtimeConnection, KitRealtimeLivenessWatchdog, toKitWebSocketUrl } from './kit-realtime-connection';

Expand Down Expand Up @@ -41,8 +43,8 @@ class TestConnection extends KitRealtimeConnection<TestEvent> {
readonly removeNetworkListener = vi.fn(() => Promise.resolve());
appListenerResolver: ((handle: PluginListenerHandle) => void) | null = null;

constructor() {
super({ clientId: 'self', openTimeoutMs: 15_000, pingIntervalMs: 30_000, livenessTimeoutMs: 70_000 });
protected override get realtimeOptions(): { clientId: string } {
return { clientId: 'self' };
}

protected get shouldConnect(): boolean {
Expand Down Expand Up @@ -103,6 +105,15 @@ class TestConnection extends KitRealtimeConnection<TestEvent> {
}
}

@Injectable()
class InheritedConstructorConnection extends KitRealtimeConnection<TestEvent> {
protected readonly shouldConnect = false;

protected buildSocketTargets(): Promise<{ url: string; protocols: string[] }[]> {
return Promise.resolve([]);
}
}

describe('KitRealtimeConnection', () => {
afterEach(() => vi.useRealTimers());

Expand All @@ -112,6 +123,11 @@ describe('KitRealtimeConnection', () => {
expect(kitRealtimeProtocols('app-v1', { authToken: 'token', clientId: 'client' })).toEqual(['app-v1', 'auth.token', 'client.client']);
});

it('can be inherited by an Angular injectable without declaring a constructor', () => {
TestBed.configureTestingModule({ providers: [InheritedConstructorConnection] });
expect(TestBed.inject(InheritedConstructorConnection)).toBeInstanceOf(InheritedConstructorConnection);
});

it('pings all targets and atomically reconnects after one closes', async () => {
vi.useFakeTimers();
const connection = new TestConnection();
Expand Down
20 changes: 15 additions & 5 deletions projects/kit/src/lib/realtime/kit-realtime-connection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { App } from '@capacitor/app';
import type { PluginListenerHandle } from '@capacitor/core';
import { Network } from '@capacitor/network';
import { Injectable } from '@angular/core';
import type { Observable } from 'rxjs';
import { Subject } from 'rxjs';

Expand Down Expand Up @@ -97,13 +98,12 @@ export class KitRealtimeLivenessWatchdog {
* suspension, exponential backoff, open/liveness timeouts, runtime-friendly application pings,
* all-target atomic reconnect, and a resync signal after connectivity is restored.
*/
@Injectable()
export abstract class KitRealtimeConnection<TEvent extends KitRealtimeEvent> {
readonly #events$ = new Subject<KitClientRealtimeEvent<TEvent>>();
readonly #reconnected$ = new Subject<void>();
readonly #options: Required<KitRealtimeConnectionOptions>;

/** Client ID used to classify self echoes. */
readonly id: string;
protected readonly listeners: PluginListenerHandle[] = [];

#sockets = new Set<WebSocket>();
Expand All @@ -125,8 +125,14 @@ export abstract class KitRealtimeConnection<TEvent extends KitRealtimeEvent> {
/** Emits once after every fully restored connection cycle, prompting consumers to resync via REST. */
readonly reconnected$: Observable<void> = this.#reconnected$.asObservable();

protected constructor(options: KitRealtimeConnectionOptions = {}) {
this.#options = {
/** Override timing/protocol defaults in specialized clients or tests. */
protected get realtimeOptions(): KitRealtimeConnectionOptions {
return {};
}

get #options(): Required<KitRealtimeConnectionOptions> {
const options = this.realtimeOptions;
return {
clientId: options.clientId ?? KIT_REALTIME_CLIENT_ID,
ping: options.ping ?? 'ping',
pong: options.pong ?? 'pong',
Expand All @@ -135,7 +141,11 @@ export abstract class KitRealtimeConnection<TEvent extends KitRealtimeEvent> {
pingIntervalMs: options.pingIntervalMs ?? 30_000,
livenessTimeoutMs: options.livenessTimeoutMs ?? 70_000,
};
this.id = this.#options.clientId;
}

/** Client ID used to classify self echoes. */
get id(): string {
return this.#options.clientId;
}

/** Whether every configured socket is currently open. */
Expand Down