Skip to content

Commit 5a51777

Browse files
authored
feat: fallback to http gateway (#231)
Signed-off-by: David Wosk <dwosk@us.ibm.com>
1 parent c4102a4 commit 5a51777

File tree

4 files changed

+183
-12
lines changed

4 files changed

+183
-12
lines changed

src/app/core.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import * as ConnectTypes from '@ibm-aspera/connect-sdk-js/dist/esm/core/types';
1717
* @returns a promise that resolves if server can connect or rejects if not
1818
*/
1919
export const testConnection = (): Promise<any> => {
20-
// FIXME: If force HTTP gateway is false this ends up preventing SDK from verifying IBM Aspera for desktop is running.
21-
if (asperaSdk.useHttpGateway || asperaSdk.useConnect) {
20+
if (asperaSdk.isReady || asperaSdk.useConnect) {
2221
return Promise.resolve(asperaSdk.globals.sdkResponseData);
2322
}
2423

@@ -215,9 +214,10 @@ export const init = (options?: InitOptions): Promise<any> => {
215214
* Use {@link registerStatusCallback} to receive status updates. Use {@link getStatus} to
216215
* read the current status synchronously at any time.
217216
*
218-
* **Desktop path**: `INITIALIZING` → `RUNNING` (app detected) or `FAILED` (timeout).
219-
* Detection continues in the background after `FAILED`. If the user launches the app
220-
* later, the status transitions to `RUNNING`.
217+
* **Desktop path**: `INITIALIZING` → `RUNNING` (app detected), `DEGRADED` (timeout but
218+
* HTTP Gateway is available as a supplementary transport), or `FAILED` (timeout, no
219+
* fallback). Detection continues in the background after `DEGRADED` or `FAILED` — if the
220+
* user launches the app later, the status transitions to `RUNNING`.
221221
*
222222
* **Connect path**: `INITIALIZING` → `RUNNING`, `FAILED`, `OUTDATED`, or
223223
* `EXTENSION_INSTALL` depending on the state of the Connect browser extension
@@ -467,15 +467,43 @@ export const deregisterActivityCallback = (id: string): void => {
467467
};
468468

469469
/**
470-
* Register a callback for getting updates about the connection status of IBM Aspera SDK.
471-
*
472-
* For example, to be notified of when the SDK loses connection with the application or connection
473-
* is re-established. This can be useful if you want to handle the case where the user quits IBM Aspera
474-
* after `init` has already been called, and want to prompt the user to relaunch the application.
470+
* Register a callback for SDK lifecycle status changes. The callback fires immediately
471+
* with the current status (if one exists) and again whenever the status changes.
472+
*
473+
* Status values:
474+
*
475+
* - `INITIALIZING` — The SDK is detecting a transfer client.
476+
* - `RUNNING` — A transfer client is ready. Full functionality is available.
477+
* - `DEGRADED` — The primary transfer client (IBM Aspera for desktop) was not detected, but HTTP
478+
* Gateway is available as a fallback. This is only available if XXX...
479+
* - `FAILED` — No transfer client could be reached. This could be because the user does
480+
* not have a transfer client installed, it is not running, or in the case of HTTP Gateway,
481+
* it was not reachable.
482+
* - `DISCONNECTED` — The transfer client was previously running but lost connection. This is specific
483+
* to IBM Aspera for desktop. For example, if the user quits the app this status will trigger.
484+
* - `OUTDATED` — (Connect only) The Connect installation needs updating.
485+
* - `EXTENSION_INSTALL` — (Connect only) The browser extension needs to be installed.
486+
*
487+
* For IBM Aspera for desktop, detection continues in the background after `FAILED` or `DEGRADED`.
488+
* If the user launches the application later, the status transitions to `RUNNING`.
475489
*
476490
* @param callback callback function to receive status events
477491
*
478492
* @returns ID representing the callback for deregistration purposes
493+
*
494+
* @example
495+
* const id = registerStatusCallback(status => {
496+
* if (status === 'RUNNING') {
497+
* // Full functionality — enable all UI
498+
* } else if (status === 'DEGRADED') {
499+
* // Transfers work via HTTP Gateway
500+
* } else if (status === 'FAILED') {
501+
* // Nothing available — prompt user to install
502+
* }
503+
* });
504+
*
505+
* // Later, to stop listening:
506+
* deregisterStatusCallback(id);
479507
*/
480508
export const registerStatusCallback = (callback: (status: SdkStatus) => void): string => {
481509
return statusService.registerCallback(callback);

src/app/status.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {SdkStatus} from '../models/models';
22
import {randomUUID} from '../helpers/helpers';
3+
import {asperaSdk} from '../index';
34

45
type StatusCallback = (status: SdkStatus) => void;
56

@@ -14,10 +15,22 @@ class StatusService {
1415
}
1516

1617
setStatus(status: SdkStatus): void {
18+
// When Desktop disconnects, remap to DEGRADED if HTTP Gateway is available
19+
if (status === 'DISCONNECTED' && asperaSdk.httpGatewayIsReady) {
20+
status = 'DEGRADED';
21+
}
22+
1723
if (this.currentStatus === status) {
1824
return;
1925
}
2026

27+
// Manage Desktop verification state based on status transitions
28+
if (status === 'DISCONNECTED' || status === 'DEGRADED') {
29+
asperaSdk.globals.asperaAppVerified = false;
30+
} else if (status === 'RUNNING' && (this.currentStatus === 'DISCONNECTED' || this.currentStatus === 'DEGRADED')) {
31+
asperaSdk.globals.asperaAppVerified = true;
32+
}
33+
2134
this.currentStatus = status;
2235
this.callbacks.forEach(cb => cb(status));
2336
}
@@ -40,15 +53,19 @@ class StatusService {
4053
*
4154
* @param detectFn async function that resolves if Desktop is found, rejects if not
4255
* @param interval ms between attempts
43-
* @param failTimeout ms before transitioning to FAILED
56+
* @param failTimeout ms before transitioning to FAILED or DEGRADED
4457
*/
4558
startPolling(detectFn: () => Promise<void>, interval: number, failTimeout: number): void {
4659
this.stopPolling();
4760
this.setStatus('INITIALIZING');
4861

4962
this.failTimeoutId = setTimeout(() => {
5063
if (this.currentStatus !== 'RUNNING') {
51-
this.setStatus('FAILED');
64+
if (asperaSdk.httpGatewayIsReady) {
65+
this.setStatus('DEGRADED');
66+
} else {
67+
this.setStatus('FAILED');
68+
}
5269
}
5370
}, failTimeout);
5471

src/models/models.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,7 @@ export type SdkStatus =
739739
| 'INITIALIZING'
740740
| 'RETRYING'
741741
| 'RUNNING'
742+
| 'DEGRADED'
742743
| 'FAILED'
743744
| 'OUTDATED'
744745
| 'EXTENSION_INSTALL'

tests/status.spec.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {statusService} from '../src/app/status';
22
import {SdkStatus} from '../src/models/models';
3+
import {asperaSdk} from '../src/index';
34

45
describe('StatusService', () => {
56
afterEach(() => {
67
statusService.reset();
8+
asperaSdk.globals.httpGatewayVerified = false;
9+
asperaSdk.globals.asperaAppVerified = false;
710
jest.useRealTimers();
811
});
912

@@ -47,6 +50,69 @@ describe('StatusService', () => {
4750

4851
expect(cb).toHaveBeenCalledWith('DISCONNECTED');
4952
});
53+
54+
test('remaps DISCONNECTED to DEGRADED when HTTP Gateway is ready', () => {
55+
asperaSdk.globals.httpGatewayVerified = true;
56+
statusService.setStatus('RUNNING');
57+
58+
statusService.setStatus('DISCONNECTED');
59+
60+
expect(statusService.getStatus()).toBe('DEGRADED');
61+
});
62+
63+
test('keeps DISCONNECTED when HTTP Gateway is not ready', () => {
64+
statusService.setStatus('RUNNING');
65+
66+
statusService.setStatus('DISCONNECTED');
67+
68+
expect(statusService.getStatus()).toBe('DISCONNECTED');
69+
});
70+
71+
test('resets asperaAppVerified on DISCONNECTED', () => {
72+
asperaSdk.globals.asperaAppVerified = true;
73+
statusService.setStatus('RUNNING');
74+
75+
statusService.setStatus('DISCONNECTED');
76+
77+
expect(asperaSdk.globals.asperaAppVerified).toBe(false);
78+
});
79+
80+
test('resets asperaAppVerified on DEGRADED', () => {
81+
asperaSdk.globals.asperaAppVerified = true;
82+
asperaSdk.globals.httpGatewayVerified = true;
83+
statusService.setStatus('RUNNING');
84+
85+
statusService.setStatus('DISCONNECTED'); // remapped to DEGRADED
86+
87+
expect(asperaSdk.globals.asperaAppVerified).toBe(false);
88+
});
89+
90+
test('restores asperaAppVerified when transitioning from DEGRADED to RUNNING', () => {
91+
asperaSdk.globals.httpGatewayVerified = true;
92+
statusService.setStatus('DEGRADED');
93+
expect(asperaSdk.globals.asperaAppVerified).toBe(false);
94+
95+
statusService.setStatus('RUNNING');
96+
97+
expect(asperaSdk.globals.asperaAppVerified).toBe(true);
98+
});
99+
100+
test('restores asperaAppVerified when transitioning from DISCONNECTED to RUNNING', () => {
101+
statusService.setStatus('DISCONNECTED');
102+
expect(asperaSdk.globals.asperaAppVerified).toBe(false);
103+
104+
statusService.setStatus('RUNNING');
105+
106+
expect(asperaSdk.globals.asperaAppVerified).toBe(true);
107+
});
108+
109+
test('does not set asperaAppVerified when transitioning to RUNNING from other states', () => {
110+
statusService.setStatus('INITIALIZING');
111+
112+
statusService.setStatus('RUNNING');
113+
114+
expect(asperaSdk.globals.asperaAppVerified).toBe(false);
115+
});
50116
});
51117

52118
describe('registerCallback', () => {
@@ -162,6 +228,65 @@ describe('StatusService', () => {
162228
expect(detectFn).toHaveBeenCalledTimes(1);
163229
});
164230

231+
test('transitions to DEGRADED when HTTP Gateway is ready', async () => {
232+
jest.useFakeTimers();
233+
asperaSdk.globals.httpGatewayVerified = true;
234+
235+
const detectFn = jest.fn().mockRejectedValue(new Error('not found'));
236+
statusService.startPolling(detectFn, 2000, 5000);
237+
238+
jest.advanceTimersByTime(5000);
239+
expect(statusService.getStatus()).toBe('DEGRADED');
240+
});
241+
242+
test('transitions to FAILED when HTTP Gateway is not ready', async () => {
243+
jest.useFakeTimers();
244+
245+
const detectFn = jest.fn().mockRejectedValue(new Error('not found'));
246+
statusService.startPolling(detectFn, 2000, 5000);
247+
248+
jest.advanceTimersByTime(5000);
249+
expect(statusService.getStatus()).toBe('FAILED');
250+
});
251+
252+
test('transitions from DEGRADED to RUNNING when detectFn eventually resolves', async () => {
253+
jest.useFakeTimers();
254+
asperaSdk.globals.httpGatewayVerified = true;
255+
256+
let callCount = 0;
257+
const detectFn = jest.fn().mockImplementation(() => {
258+
callCount++;
259+
if (callCount <= 3) {
260+
return Promise.reject(new Error('not found'));
261+
}
262+
return Promise.resolve();
263+
});
264+
265+
const statuses: SdkStatus[] = [];
266+
statusService.registerCallback((s) => statuses.push(s));
267+
268+
statusService.startPolling(detectFn, 2000, 5000);
269+
270+
// First attempt fails
271+
await Promise.resolve();
272+
expect(statusService.getStatus()).toBe('INITIALIZING');
273+
274+
// Advance past timeout
275+
jest.advanceTimersByTime(5000);
276+
await Promise.resolve();
277+
expect(statusService.getStatus()).toBe('DEGRADED');
278+
279+
// Next poll interval triggers attempt that succeeds
280+
jest.advanceTimersByTime(2000);
281+
await Promise.resolve();
282+
await Promise.resolve();
283+
284+
expect(statusService.getStatus()).toBe('RUNNING');
285+
expect(statuses).toContain('INITIALIZING');
286+
expect(statuses).toContain('DEGRADED');
287+
expect(statuses).toContain('RUNNING');
288+
});
289+
165290
test('transitions from FAILED to RUNNING when detectFn eventually resolves', async () => {
166291
jest.useFakeTimers();
167292

0 commit comments

Comments
 (0)