Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release: Amplify JS release #14004

Merged
merged 6 commits into from
Nov 12, 2024
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
71 changes: 71 additions & 0 deletions packages/api-graphql/__tests__/AWSAppSyncEventProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,77 @@ describe('AppSyncEventProvider', () => {
'Connection failed: Retriable Test',
);
});

test('subscription observer is triggered when a connection is formed and a data message is received after connection ack', async () => {
expect.assertions(1);
const mockNext = jest.fn();

const observer = provider.subscribe({
appSyncGraphqlEndpoint: 'ws://localhost:8080',
});

const event = JSON.stringify({ some: 'data' });

observer.subscribe({
// Succeed only when the first message comes through
next: mockNext,
// Closing a hot connection (for cleanup) makes it blow up the test stack
error: () => {},
});
await fakeWebSocketInterface?.standardConnectionHandshake();
await fakeWebSocketInterface?.startAckMessage({
connectionTimeoutMs: 100,
});
await fakeWebSocketInterface?.sendDataMessage({
id: fakeWebSocketInterface?.webSocket.subscriptionId,
type: MESSAGE_TYPES.DATA,
event,
});

// events callback returns entire message contents
expect(mockNext).toHaveBeenCalledWith({
id: fakeWebSocketInterface?.webSocket.subscriptionId,
type: MESSAGE_TYPES.DATA,
event: JSON.parse(event),
});
});

test('socket is disconnected after .close() is called', async () => {
expect.assertions(2);
const mockNext = jest.fn();

const observer = provider.subscribe({
appSyncGraphqlEndpoint: 'ws://localhost:8080',
});

const event = JSON.stringify({ some: 'data' });

observer.subscribe({
next: mockNext,
error: () => {},
});

await fakeWebSocketInterface?.standardConnectionHandshake();
await fakeWebSocketInterface?.startAckMessage({
connectionTimeoutMs: 100,
});
await fakeWebSocketInterface?.sendDataMessage({
id: fakeWebSocketInterface?.webSocket.subscriptionId,
type: MESSAGE_TYPES.DATA,
event,
});

// events callback returns entire message contents
expect(mockNext).toHaveBeenCalledWith({
id: fakeWebSocketInterface?.webSocket.subscriptionId,
type: MESSAGE_TYPES.DATA,
event: JSON.parse(event),
});

await provider.close();

expect(fakeWebSocketInterface.hasClosed).resolves.toBeUndefined();
});
});
});
});
Expand Down
7 changes: 5 additions & 2 deletions packages/api-graphql/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,8 @@ export class FakeWebSocketInterface {
/**
* Run a command and resolve to allow internal behavior to execute
*/
async runAndResolve(fn) {
async runAndResolve(fn: Function) {
await fn();
await Promise.resolve();
}

/**
Expand Down Expand Up @@ -310,6 +309,10 @@ class FakeWebSocket implements WebSocket {
close(code?: number, reason?: string): void {
const closeResolver = this.closeResolverFcn();
if (closeResolver) closeResolver(Promise.resolve(undefined));

try {
this.onclose(new CloseEvent('', {}));
} catch {}
}
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
const parsedInput = JSON.parse(String(data));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ interface DataResponse {

const PROVIDER_NAME = 'AWSAppSyncEventsProvider';
const WS_PROTOCOL_NAME = 'aws-appsync-event-ws';
const CONNECT_URI = ''; // events does not expect a connect uri

export class AWSAppSyncEventProvider extends AWSWebSocketProvider {
constructor() {
super({ providerName: PROVIDER_NAME, wsProtocolName: WS_PROTOCOL_NAME });
super({
providerName: PROVIDER_NAME,
wsProtocolName: WS_PROTOCOL_NAME,
connectUri: CONNECT_URI,
});
}

getProviderName() {
Expand Down Expand Up @@ -90,7 +95,6 @@ export class AWSAppSyncEventProvider extends AWSWebSocketProvider {
appSyncGraphqlEndpoint,
authenticationType,
query,
variables,
apiKey,
region,
} = options;
Expand All @@ -100,7 +104,7 @@ export class AWSAppSyncEventProvider extends AWSWebSocketProvider {
// events: [variables],
// };

const serializedData = JSON.stringify([variables]);
const serializedData = JSON.stringify({ channel: query });

const headers = {
...(await awsRealTimeHeaderBasedAuth({
Expand Down Expand Up @@ -167,7 +171,7 @@ export class AWSAppSyncEventProvider extends AWSWebSocketProvider {
if (type === MESSAGE_TYPES.DATA && payload) {
const deserializedEvent = JSON.parse(payload);
if (observer) {
observer.next(deserializedEvent);
observer.next({ id, type, event: deserializedEvent });
} else {
this.logger.debug(`observer not found for id: ${id}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ interface DataPayload {

const PROVIDER_NAME = 'AWSAppSyncRealTimeProvider';
const WS_PROTOCOL_NAME = 'graphql-ws';
const CONNECT_URI = '/connect';

export class AWSAppSyncRealTimeProvider extends AWSWebSocketProvider {
constructor() {
super({ providerName: PROVIDER_NAME, wsProtocolName: WS_PROTOCOL_NAME });
super({
providerName: PROVIDER_NAME,
wsProtocolName: WS_PROTOCOL_NAME,
connectUri: CONNECT_URI,
});
}

getProviderName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,14 @@ interface ParsedMessagePayload {
interface AWSWebSocketProviderArgs {
providerName: string;
wsProtocolName: string;
connectUri: string;
}

export abstract class AWSWebSocketProvider {
protected logger: ConsoleLogger;
protected subscriptionObserverMap = new Map<string, ObserverQuery>();

private awsRealTimeSocket?: WebSocket;
protected awsRealTimeSocket?: WebSocket;
private socketStatus: SOCKET_STATUS = SOCKET_STATUS.CLOSED;
private keepAliveTimeoutId?: ReturnType<typeof setTimeout>;
private keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT;
Expand All @@ -91,10 +92,12 @@ export abstract class AWSWebSocketProvider {
private readonly reconnectionMonitor = new ReconnectionMonitor();
private connectionStateMonitorSubscription: SubscriptionLike;
private readonly wsProtocolName: string;
private readonly wsConnectUri: string;

constructor(args: AWSWebSocketProviderArgs) {
this.logger = new ConsoleLogger(args.providerName);
this.wsProtocolName = args.wsProtocolName;
this.wsConnectUri = args.connectUri;

this.connectionStateMonitorSubscription =
this._startConnectionStateMonitoring();
Expand All @@ -112,6 +115,24 @@ export abstract class AWSWebSocketProvider {
this.connectionStateMonitorSubscription.unsubscribe();
// Complete all reconnect observers
this.reconnectionMonitor.close();

return new Promise<void>((resolve, reject) => {
if (this.awsRealTimeSocket) {
this.awsRealTimeSocket.onclose = (_: CloseEvent) => {
this.subscriptionObserverMap = new Map();
this.awsRealTimeSocket = undefined;
resolve();
};

this.awsRealTimeSocket.onerror = (err: any) => {
reject(err);
};

this.awsRealTimeSocket.close();
} else {
resolve();
}
});
}

subscribe(
Expand Down Expand Up @@ -721,7 +742,7 @@ export abstract class AWSWebSocketProvider {
const authHeader = await awsRealTimeHeaderBasedAuth({
authenticationType,
payload: payloadString,
canonicalUri: '/connect',
canonicalUri: this.wsConnectUri,
apiKey,
appSyncGraphqlEndpoint,
region,
Expand Down
15 changes: 13 additions & 2 deletions packages/api-graphql/src/internals/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,19 @@ async function post(
}
}

function closeAll(): void {
eventProvider.close();
/**
* @experimental API may change in future versions
*
* Close WebSocket connection, disconnect listeners and reconnect observers
*
* @example
* await events.closeAll()
*
* @returns void on success
* @throws on error
*/
async function closeAll(): Promise<void> {
await eventProvider.close();
}

export { connect, post, closeAll };
6 changes: 3 additions & 3 deletions packages/aws-amplify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@
"name": "[Analytics] record (Pinpoint)",
"path": "./dist/esm/analytics/index.mjs",
"import": "{ record }",
"limit": "17.41 kB"
"limit": "17.5 kB"
},
{
"name": "[Analytics] record (Kinesis)",
Expand All @@ -317,7 +317,7 @@
"name": "[Analytics] identifyUser (Pinpoint)",
"path": "./dist/esm/analytics/index.mjs",
"import": "{ identifyUser }",
"limit": "15.91 kB"
"limit": "15.95 kB"
},
{
"name": "[Analytics] enable",
Expand Down Expand Up @@ -497,7 +497,7 @@
"name": "[Storage] uploadData (S3)",
"path": "./dist/esm/storage/index.mjs",
"import": "{ uploadData }",
"limit": "20.08 kB"
"limit": "20.15 kB"
}
]
}
22 changes: 22 additions & 0 deletions packages/core/__tests__/storage/DefaultStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DefaultStorage } from '../../src/storage/DefaultStorage';
import { InMemoryStorage } from '../../src/storage/InMemoryStorage';

const key = 'k';
const value = 'value';
Expand Down Expand Up @@ -35,4 +36,25 @@ describe('DefaultStorage', () => {
await defaultStorage.clear();
expect(defaultStorage.getItem(key)).resolves.toBeNull();
});

it('should fall back to alternative storage when localStorage is not accessible', async () => {
// Mock window.localStorage to throw an error
const originalLocalStorage = window.localStorage;

Object.defineProperty(window, 'localStorage', {
value: undefined,
writable: true,
});

// Create a new DefaultStorage instance to trigger the fallback
const fallbackStorage = new DefaultStorage();

// Verify that the storage still works as expected
expect(fallbackStorage.storage instanceof InMemoryStorage).toEqual(true);

// Restore the original localStorage
Object.defineProperty(window, 'localStorage', {
value: originalLocalStorage,
});
});
});
24 changes: 23 additions & 1 deletion packages/core/__tests__/storage/SessionStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { InMemoryStorage } from '../../src/storage/InMemoryStorage';
import { SessionStorage } from '../../src/storage/SessionStorage';

const key = 'k';
const value = 'value';

describe('sessionStorage', () => {
describe('SessionStorage', () => {
let sessionStorage: SessionStorage;

beforeEach(() => {
Expand Down Expand Up @@ -37,4 +38,25 @@ describe('sessionStorage', () => {
await sessionStorage.clear();
expect(await sessionStorage.getItem(key)).toBeNull();
});

it('should fall back to alternative storage when sessionStorage is not accessible', async () => {
// Mock window.sessionStorage to throw an error
const originalSessionStorage = window.sessionStorage;

Object.defineProperty(window, 'sessionStorage', {
value: undefined,
writable: true,
});

// Create a new SessionStorage instance to trigger the fallback
const fallbackStorage = new SessionStorage();

// Verify that the storage still works as expected
expect(fallbackStorage.storage instanceof InMemoryStorage).toEqual(true);

// Restore the original sessionStorage
Object.defineProperty(window, 'sessionStorage', {
value: originalSessionStorage,
});
});
});
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
"name": "Cache (default browser storage)",
"path": "./dist/esm/index.mjs",
"import": "{ Cache }",
"limit": "3.3 kB"
"limit": "3.4 kB"
}
],
"exports": {
Expand Down
Loading
Loading