Skip to content

Commit

Permalink
feat(core): queued storage (Web) for logger
Browse files Browse the repository at this point in the history
  • Loading branch information
HuiSF committed Jan 4, 2024
1 parent 46038f7 commit 912924f
Show file tree
Hide file tree
Showing 7 changed files with 615 additions and 0 deletions.
380 changes: 380 additions & 0 deletions packages/core/__tests__/utils/queuedStorage/queuedStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,380 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {
DATABASE_NAME,
STORE_NAME,
} from '../../../src/utils/queuedStorage/constants';
import { createQueuedStorage } from '../../../src/utils/queuedStorage/createQueuedStorage';
import { getAddItemBytesSize } from '../../../src/utils/queuedStorage/getAddItemBytesSize';
import {
AddItem,
QueuedItem,
QueuedStorage,
} from '../../../src/utils/queuedStorage/types';

describe('createQueuedStorage', () => {
let originalIndexedDB;
let originalIDBKeyRange;
const mockTimestamp = new Date('2024-01-02').toUTCString();
const mockAdd = jest.fn(() => ({
set onsuccess(handler) {
handler();
},
set onerror(handler) {
handler();
},
}));
const mockClear = jest.fn();
const mockGetAll = jest.fn(() => ({
set onsuccess(handler) {
handler();
},
set onerror(handler) {
handler();
},
result: [] as QueuedItem[],
}));
const mockDelete = jest.fn(() => ({
set onsuccess(handler) {
handler();
},
set onerror(handler) {
handler();
},
}));
const mockObjectStore = jest.fn(() => ({
add: mockAdd,
delete: mockDelete,
clear: mockClear,
getAll: mockGetAll,
}));
const mockTransaction = jest.fn(() => ({
objectStore: mockObjectStore,
}));
const mockDB = {
transaction: mockTransaction,
objectStoreNames: {
contains: jest.fn(),
},
createObjectStore: jest.fn(),
};
const mockIndexDBOpenRequest = {
set onupgradeneeded(handler) {
handler();
},
set onsuccess(handler) {
handler();
},
set onerror(_) {},
result: mockDB,
};
const mockIndexedDBOpen = jest.fn(() => mockIndexDBOpenRequest);
const mockIndexedDB = {
open: mockIndexedDBOpen,
};
const mockIDBKeyRange = {
bound: jest.fn((lower, upper) => [lower, upper]),
};

let queuedStorage: QueuedStorage;

beforeAll(() => {
mockClear.mockResolvedValue(undefined);

originalIndexedDB = window.indexedDB;
originalIDBKeyRange = window.IDBKeyRange;
window.indexedDB = mockIndexedDB as any;
window.IDBKeyRange = mockIDBKeyRange as any;
});

afterAll(() => {
window.indexedDB = originalIndexedDB;
window.IDBKeyRange = originalIDBKeyRange;
});

describe('initialization', () => {
const testBytesSize = 1;
const mockQueuedItems = [
{
id: 1,
content: '123',
timestamp: mockTimestamp,
get bytesSize() {
return (testBytesSize / 2) * 1024 * 1024;
},
},
{
id: 2,
content: '1234',
timestamp: mockTimestamp,
get bytesSize() {
return (testBytesSize / 2) * 1024 * 1024;
},
},
];

beforeAll(async () => {
mockGetAll.mockReturnValueOnce({
set onsuccess(handler) {
handler();
},
set onerror(_) {},
result: mockQueuedItems,
});
queuedStorage = createQueuedStorage();
});

afterAll(() => {
mockGetAll.mockClear();
});

it('creates a queued storage with expected apis', () => {
expect(queuedStorage).toMatchObject({
add: expect.any(Function),
peek: expect.any(Function),
peekAll: expect.any(Function),
delete: expect.any(Function),
clear: expect.any(Function),
isFull: expect.any(Function),
});
});

it('invokes window.indexedDB related APIs set up the database', async () => {
// open database
expect(mockIndexedDBOpen).toHaveBeenCalledTimes(1);
expect(mockIndexedDBOpen).toHaveBeenCalledWith(DATABASE_NAME, 1);

// create data store
expect(mockDB.objectStoreNames.contains).toHaveBeenCalledTimes(1);
expect(mockDB.objectStoreNames.contains).toHaveBeenCalledWith(STORE_NAME);
expect(mockDB.createObjectStore).toHaveBeenCalledTimes(1);
expect(mockDB.createObjectStore).toHaveBeenCalledWith(STORE_NAME, {
keyPath: 'id',
autoIncrement: true,
});

// calculate current store bytes size on load
expect(mockDB.transaction).toHaveBeenCalledTimes(1); // 2: 1. calculate bytes size 2. call of queuedStorage.clear()
expect(mockDB.transaction).toHaveBeenCalledWith(STORE_NAME, 'readonly');
});

it('calculates the queue item bytes size', () => {
expect(queuedStorage.isFull(testBytesSize)).toBe(true);
});
});

describe('error opening indexedDB', () => {
const expectedError = new DOMException('some error');

beforeEach(() => {
mockIndexedDBOpen.mockReturnValueOnce({
set onupgradeneeded(handler) {
handler();
},
set onsuccess(_) {},
set onerror(handler) {
handler();
},
error: expectedError,
result: mockDB,
} as any);
});

test.each([
['add', {}],
['peek', 1],
['peekAll', undefined],
['delete', [{}]],
['clear', undefined],
])('when invokes %s it throws', async (method, args) => {
const storage = createQueuedStorage();
await expect(storage[method](args)).rejects.toThrow(expectedError);
});
});

describe('method add()', () => {
const testInput: AddItem = {
content: 'some log content',
timestamp: mockTimestamp,
};

afterEach(() => {
mockAdd.mockClear();
mockGetAll.mockClear();
mockDelete.mockClear();
mockIDBKeyRange.bound.mockClear();
});

it('adds item to the indexedDB', async () => {
await queuedStorage.add(testInput);

expect(mockAdd).toHaveBeenCalledTimes(1);
expect(mockAdd).toHaveBeenCalledWith({
content: 'some log content',
timestamp: mockTimestamp,
bytesSize: getAddItemBytesSize(testInput),
});
});

it('dequeues the first item before adds the new item', async () => {
const mockQueuedItems = [
{
id: 3,
content: '123',
timestamp: mockTimestamp,
bytesSize: 1,
},
];

mockGetAll.mockReturnValueOnce({
set onsuccess(handler) {
handler();
},
set onerror(_) {},
result: mockQueuedItems,
});

await queuedStorage.add(testInput, { dequeueBeforeEnqueue: true });

expect(mockDelete).toHaveBeenCalledTimes(1);
expect(mockDelete).toHaveBeenCalledWith([3, 3]); // id: 3 - range 3, 3
expect(mockAdd).toHaveBeenCalledTimes(1);
expect(mockAdd).toHaveBeenCalledWith({
content: 'some log content',
timestamp: mockTimestamp,
bytesSize: getAddItemBytesSize(testInput),
});
});
});

describe('method peek() and peekAll()', () => {
const mockQueuedItems = [
{
id: 3,
content: '123',
timestamp: mockTimestamp,
bytesSize: 1,
},
{
id: 5,
content: '123',
timestamp: mockTimestamp,
bytesSize: 1,
},
];

afterEach(() => {
mockGetAll.mockClear();
});

test('peek() returns specified number of queued items', async () => {
mockGetAll.mockReturnValueOnce({
set onsuccess(handler) {
handler();
},
set onerror(_) {},
result: [mockQueuedItems[0]],
});
const result = await queuedStorage.peek(1);

expect(mockGetAll).toHaveBeenCalledTimes(1);
expect(mockGetAll).toHaveBeenCalledWith(undefined, 1);
expect(result).toHaveLength(1);
});

test('peekAll() returns all queued items', async () => {
mockGetAll.mockReturnValueOnce({
set onsuccess(handler) {
handler();
},
set onerror(_) {},
result: mockQueuedItems,
});

const result = await queuedStorage.peekAll();
expect(mockGetAll).toHaveBeenCalledTimes(1);
expect(mockGetAll).toHaveBeenCalledWith(undefined, undefined);
expect(result).toHaveLength(mockQueuedItems.length);
});
});

describe('method delete()', () => {
const testItems = [
{
id: 2,
content: '123',
timestamp: mockTimestamp,
bytesSize: 1,
},
{
id: 5,
content: '123',
timestamp: mockTimestamp,
bytesSize: 1,
},
{
id: 6,
content: '123',
timestamp: mockTimestamp,
bytesSize: 1,
},
{
id: 7,
content: '123',
timestamp: mockTimestamp,
bytesSize: 1,
},
{
id: 11,
content: '123',
timestamp: mockTimestamp,
bytesSize: 1,
},
{
id: 12,
content: '123',
timestamp: mockTimestamp,
bytesSize: 1,
},
];

afterEach(() => {
mockDelete.mockClear();
mockIDBKeyRange.bound.mockClear();
});

it('deletes items by key ranges', async () => {
await queuedStorage.delete(testItems);

expect(mockIDBKeyRange.bound).toHaveBeenCalledTimes(3);
expect(mockIDBKeyRange.bound.mock.calls).toEqual([
[2, 2],
[5, 7],
[11, 12],
]);

expect(mockDelete).toHaveBeenCalledTimes(3);
expect(mockDelete.mock.calls).toEqual([[[2, 2]], [[5, 7]], [[11, 12]]]);
});

it('does nothing when there is nothing to delete', async () => {
await queuedStorage.delete([]);

expect(mockIDBKeyRange.bound).not.toHaveBeenCalled();
expect(mockDelete).not.toHaveBeenCalled();
});
});

describe('method clear()', () => {
afterEach(() => {
mockClear.mockClear();
});

it('invokes indexedDB store clear method', async () => {
await queuedStorage.clear();

expect(mockClear).toHaveBeenCalledTimes(1);
});
});
});
5 changes: 5 additions & 0 deletions packages/core/src/utils/queuedStorage/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export const DATABASE_NAME = 'amplify_logging_cloudwatch';
export const STORE_NAME = 'logging_cached_logs';
Loading

0 comments on commit 912924f

Please sign in to comment.