Skip to content

Commit

Permalink
release: Amplify JS release (#14004)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuhengshs authored Nov 12, 2024
2 parents c92c928 + 4606279 commit 2797deb
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 24 deletions.
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
25 changes: 23 additions & 2 deletions packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts
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

0 comments on commit 2797deb

Please sign in to comment.