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
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@ Sponsoring means you directly contribute to new features, improvements, and main
## Support Version

| Angular | Package version |
|---------|-----------------|
| ------- | --------------- |
| v20 | 20.x.x |
| v19 | 19.x.x |
| v18 | 2.x.x |


## packages

| package name | description | path |
|-------------------------------------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| @rdlabo/ionic-angular-kit | Auth guards, Firebase flows, storage, overlay, HTTP interceptor, and other fleet helpers. | [/projects/kit](https://github.com/rdlabo-team/ionic-angular-library/tree/main/projects/kit#readme) |
| @rdlabo/ionic-angular-photo-editor | This is a photo editor and viewer for modal page of Ionic Angular project using Capacitor. | [/project/photo-editor](https://github.com/rdlabo-team/ionic-angular-library/tree/main/projects/photo-editor#readme) |
| @rdlabo/ionic-angular-scroll-header | This is directive for scroll with Header. | [/project/scroll-header](https://github.com/rdlabo-team/ionic-angular-library/tree/main/projects/scroll-header#readme) |
| @rdlabo/ngx-cdk-scroll-strategies | This is directive for virtual scroll of dynamic item size. | [/project/scroll-strategies](https://github.com/rdlabo-team/ionic-angular-library/tree/main/projects/scroll-strategies#readme) |
| package name | description | path |
| ----------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| @rdlabo/ionic-angular-kit | Auth guards, Firebase flows, storage, overlay, HTTP interceptor, and other fleet helpers. | [/projects/kit](https://github.com/rdlabo-team/ionic-angular-library/tree/main/projects/kit#readme) |
| @rdlabo/ionic-angular-photo-editor | This is a photo editor and viewer for modal page of Ionic Angular project using Capacitor. | [/project/photo-editor](https://github.com/rdlabo-team/ionic-angular-library/tree/main/projects/photo-editor#readme) |
| @rdlabo/ionic-angular-scroll-header | This is directive for scroll with Header. | [/project/scroll-header](https://github.com/rdlabo-team/ionic-angular-library/tree/main/projects/scroll-header#readme) |
| @rdlabo/ngx-cdk-scroll-strategies | This is directive for virtual scroll of dynamic item size. | [/project/scroll-strategies](https://github.com/rdlabo-team/ionic-angular-library/tree/main/projects/scroll-strategies#readme) |

### Release

Expand All @@ -43,7 +42,6 @@ The demo app includes a **Kit** tab with a Firebase Auth harness (`/main/kit/aut
3. `npm run e2e` — Playwright signs up with a UUID email; `window.__E2E__` skips email confirmation.
4. `npm run cap` — copy a production build to iOS/Android for device checks (e.g. `kitAuthInput` autofill).


## sponsors

This is an Apache-2.0-licensed open source project. It can grow thanks to the support by these awesome people. If you'd like to join them, please read more [here](https://github.com/sponsors/rdlabo-team) .
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rdlabo/ionic-angular-library",
"version": "21.3.2",
"version": "21.3.3",
"repository": {
"type": "git",
"url": "git@github.com:rdlabo-team/ionic-angular-library.git"
Expand Down
2 changes: 1 addition & 1 deletion projects/kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Feature-scoped peers are only needed by the features that use them (`status-bar`

An abstract Hibernation WebSocket client for application realtime services. Subclasses supply
connection intent and one or more `{ url, protocols }` targets; the kit owns foreground/network
suspension, all-target reconnect, exponential backoff, open and half-open detection, ping/pong,
suspension, target-scoped reconnect that preserves healthy sockets, exponential backoff, open and half-open detection, ping/pong,
self-echo annotation, and `reconnected$` resync signaling. Use `kitRealtimeProtocols()` to pass
authentication and the stable `KIT_REALTIME_CLIENT_ID` through WebSocket subprotocols without
putting credentials in the URL.
Expand Down
2 changes: 1 addition & 1 deletion projects/kit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rdlabo/ionic-angular-kit",
"version": "21.3.2",
"version": "21.3.3",
"repository": {
"type": "git",
"url": "https://github.com/rdlabo-team/ionic-angular-library.git"
Expand Down
100 changes: 97 additions & 3 deletions projects/kit/src/lib/realtime/kit-realtime-connection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class TestConnection extends KitRealtimeConnection<TestEvent> {
targetCount = 1;
failTargets = false;
failFailureHook = false;
pendingFailureHook: Promise<void> | null = null;
failureCalls = 0;
readonly sockets: FakeWebSocket[] = [];
readonly removeAppListener = vi.fn(() => Promise.resolve());
Expand Down Expand Up @@ -65,6 +66,9 @@ class TestConnection extends KitRealtimeConnection<TestEvent> {

protected override handleConnectionFailure(): Promise<void> {
this.failureCalls += 1;
if (this.pendingFailureHook) {
return this.pendingFailureHook;
}
if (this.failFailureHook) {
return Promise.reject(new Error('storage unavailable'));
}
Expand Down Expand Up @@ -128,7 +132,7 @@ describe('KitRealtimeConnection', () => {
expect(TestBed.inject(InheritedConstructorConnection)).toBeInstanceOf(InheritedConstructorConnection);
});

it('pings all targets and atomically reconnects after one closes', async () => {
it('pings all targets and reconnects only the target that closes', async () => {
vi.useFakeTimers();
const connection = new TestConnection();
connection.targetCount = 2;
Expand All @@ -141,7 +145,8 @@ describe('KitRealtimeConnection', () => {
connection.sockets[0].close();
await vi.runAllTicks();
await vi.advanceTimersByTimeAsync(1000);
expect(connection.sockets).toHaveLength(4);
expect(connection.sockets).toHaveLength(3);
expect(connection.sockets[1].readyState).toBe(WebSocket.OPEN);
connection.stop();
});

Expand Down Expand Up @@ -181,15 +186,104 @@ describe('KitRealtimeConnection', () => {
const connection = new TestConnection();
connection.targetCount = 2;
const reconnected = vi.fn();
const events: TestEvent[] = [];
connection.reconnected$.subscribe(reconnected);
connection.events$.subscribe((event) => events.push(event));
await connection.openForTest();
connection.sockets[0].open();
connection.sockets[1].close();
await vi.runAllTicks();
await vi.advanceTimersByTimeAsync(1000);
connection.sockets[2].open();
connection.sockets[3].open();
connection.sockets[0].message(JSON.stringify({ topic: 'healthy-target' }));
expect(reconnected).toHaveBeenCalledOnce();
expect(events).toContainEqual(expect.objectContaining({ topic: 'healthy-target' }));
connection.stop();
});

it('does not amplify repeated single-target failures into all-target reconnect waves', async () => {
vi.useFakeTimers();
const connection = new TestConnection();
connection.targetCount = 20;
await connection.openForTest();
connection.sockets.forEach((socket) => socket.open());
let failedSocket = connection.sockets[0];

for (let attempt = 0; attempt < 100; attempt += 1) {
if (attempt % 20 === 0) {
connection.sockets.filter((socket) => socket.readyState === WebSocket.OPEN).forEach((socket) => socket.message('pong'));
}
failedSocket.close();
await vi.runAllTicks();
await vi.advanceTimersByTimeAsync(1000);
failedSocket = connection.sockets.at(-1)!;
failedSocket.open();
}

expect(connection.sockets).toHaveLength(120);
expect(connection.sockets.slice(1, 20).every((socket) => socket.readyState === WebSocket.OPEN)).toBe(true);
connection.stop();
});

it('closes every remaining socket when a retry resolves to an empty target set', async () => {
vi.useFakeTimers();
const connection = new TestConnection();
connection.targetCount = 2;
await connection.openForTest();
connection.sockets.forEach((socket) => socket.open());
connection.targetCount = 0;

connection.sockets[0].close();
await vi.runAllTicks();
await vi.advanceTimersByTimeAsync(1000);

expect(connection.sockets[1].readyState).toBe(WebSocket.CLOSED);
expect(connection.isStreamOpen).toBe(false);
connection.stop();
});

it('coalesces concurrent target failures into one retry while retaining healthy targets', async () => {
vi.useFakeTimers();
const connection = new TestConnection();
connection.targetCount = 3;
await connection.openForTest();
connection.sockets.forEach((socket) => socket.open());

connection.sockets[0].close();
connection.sockets[1].close();
await vi.runAllTicks();
await vi.advanceTimersByTimeAsync(1000);

expect(connection.failureCalls).toBe(1);
expect(connection.sockets).toHaveLength(5);
expect(connection.sockets[2].readyState).toBe(WebSocket.OPEN);
connection.stop();
});

it('ignores a failure hook that resolves after a new connection generation starts', async () => {
vi.useFakeTimers();
let resolveOldFailure!: () => void;
const connection = new TestConnection();
connection.pendingFailureHook = new Promise<void>((resolve) => {
resolveOldFailure = resolve;
});
await connection.openForTest();
connection.sockets[0].open();

connection.sockets[0].close();
await vi.runAllTicks();
connection.stop();
connection.connectEnabled = true;
connection.pendingFailureHook = null;
await connection.openForTest();
connection.sockets[1].open();

resolveOldFailure();
await vi.runAllTicks();
await vi.advanceTimersByTimeAsync(60_000);

expect(connection.sockets).toHaveLength(2);
expect(connection.sockets[1].readyState).toBe(WebSocket.OPEN);
connection.stop();
});

Expand Down
Loading