Skip to content

Commit

Permalink
feat(azure-adapter): split transactions by chunks
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Jan 31, 2024
1 parent 3f7d455 commit c0d1761
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 126 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat(azure-adapter): split transactions by chunks",
"packageName": "monosize-storage-azure",
"email": "[email protected]",
"dependentChangeType": "patch"
}
11 changes: 0 additions & 11 deletions packages/monosize-storage-azure/src/__fixture__/sampleReport.mts

This file was deleted.

26 changes: 26 additions & 0 deletions packages/monosize-storage-azure/src/__fixture__/sampleReports.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { BundleSizeReport } from 'monosize';

export const sampleReport: BundleSizeReport = [
{
packageName: '@scope/foo-package',
name: 'Foo',
path: 'foo.fixture.js',
minifiedSize: 1000,
gzippedSize: 100,
},
{
packageName: '@scope/bar-package',
name: 'Bar',
path: 'bar.fixture.js',
minifiedSize: 1000,
gzippedSize: 100,
},
];

export const bigReport: BundleSizeReport = new Array(200).fill(null).map((_, index) => ({
packageName: '@scope/foo-package',
name: `Entry [${index}]`,
path: `foo-${index}.fixture.js`,
minifiedSize: 1000,
gzippedSize: 100,
}));
40 changes: 40 additions & 0 deletions packages/monosize-storage-azure/src/getRemoteReport.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fetch from 'node-fetch';
import pc from 'picocolors';
import type { BundleSizeReportEntry, StorageAdapter } from 'monosize';

import type { AzureStorageConfig } from './types.mjs';

const MAX_HTTP_ATTEMPT_COUNT = 5;

export function createGetRemoteReport(config: AzureStorageConfig) {
async function getRemoteReport(branch: string, attempt = 1): ReturnType<StorageAdapter['getRemoteReport']> {
try {
const response = await fetch(`${config.endpoint}?branch=${branch}`);
const result = (await response.json()) as Array<BundleSizeReportEntry & { commitSHA: string }>;

const remoteReport = result.map(entity => {
const { commitSHA, ...rest } = entity;

Check warning on line 16 in packages/monosize-storage-azure/src/getRemoteReport.mts

View workflow job for this annotation

GitHub Actions / ci

'commitSHA' is assigned a value but never used
return rest;
});
const { commitSHA } = result[result.length - 1];

return { commitSHA, remoteReport };
} catch (err) {
console.log([pc.yellow('[w]'), (err as Error).toString()].join(' '));
console.log([pc.yellow('[w]'), 'Failed to fetch report from the remote. Retrying...'].join(' '));

if (attempt >= MAX_HTTP_ATTEMPT_COUNT) {
console.error(
[pc.red('[e]'), 'Exceeded 5 attempts to fetch reports, please check previously reported warnings...'].join(
' ',
),
);
throw err;
}

return getRemoteReport(branch, attempt + 1);
}
}

return getRemoteReport;
}
10 changes: 5 additions & 5 deletions packages/monosize-storage-azure/src/getRemoteReport.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ jest.mock('node-fetch', () => fetch);

import type { Response } from 'node-fetch';

import createAzureStorage from './index.mjs';
import type { AzureStorageConfig } from './index.mjs';
import { sampleReport } from './__fixture__/sampleReport.mjs';
import { createGetRemoteReport } from './getRemoteReport.mjs';
import type { AzureStorageConfig } from './types.mjs';
import { sampleReport } from './__fixture__/sampleReports.mjs';

const testConfig: AzureStorageConfig = {
endpoint: 'https://localhost',
Expand All @@ -28,7 +28,7 @@ describe('getRemoteReport', () => {
};
fetch.mockImplementation(() => Promise.resolve(value));

const { getRemoteReport } = createAzureStorage(testConfig);
const getRemoteReport = createGetRemoteReport(testConfig);
const { remoteReport } = await getRemoteReport('main');

expect(fetch).toHaveBeenCalledTimes(1);
Expand All @@ -48,7 +48,7 @@ describe('getRemoteReport', () => {

jest.spyOn(console, 'log').mockImplementation(noop);

const { getRemoteReport } = createAzureStorage(testConfig);
const getRemoteReport = createGetRemoteReport(testConfig);
const { remoteReport } = await getRemoteReport('main');

expect(fetch).toHaveBeenCalledTimes(3);
Expand Down
115 changes: 5 additions & 110 deletions packages/monosize-storage-azure/src/index.mts
Original file line number Diff line number Diff line change
@@ -1,117 +1,12 @@
import { odata, TableClient, AzureNamedKeyCredential, TableTransaction } from '@azure/data-tables';
import fetch from 'node-fetch';
import pc from 'picocolors';
import type { BundleSizeReportEntry, StorageAdapter } from 'monosize';
import type { StorageAdapter } from 'monosize';

export type AzureStorageConfig = {
endpoint: string;
};

const MAX_HTTP_ATTEMPT_COUNT = 5;
import { createGetRemoteReport } from './getRemoteReport.mjs';
import { uploadReportToRemote } from './uploadReportToRemote.mjs';
import type { AzureStorageConfig } from './types.mjs';

function createAzureStorage(config: AzureStorageConfig): StorageAdapter {
async function getRemoteReport(branch: string, attempt = 1): ReturnType<StorageAdapter['getRemoteReport']> {
try {
const response = await fetch(`${config.endpoint}?branch=${branch}`);
const result = (await response.json()) as Array<BundleSizeReportEntry & { commitSHA: string }>;

const remoteReport = result.map(entity => {
const { commitSHA, ...rest } = entity;
return rest;
});
const { commitSHA } = result[result.length - 1];

return { commitSHA, remoteReport };
} catch (err) {
console.log([pc.yellow('[w]'), (err as Error).toString()].join(' '));
console.log([pc.yellow('[w]'), 'Failed to fetch report from the remote. Retrying...'].join(' '));

if (attempt >= MAX_HTTP_ATTEMPT_COUNT) {
console.error(
[pc.red('[e]'), 'Exceeded 5 attempts to fetch reports, please check previously reported warnings...'].join(
' ',
),
);
throw err;
}

return getRemoteReport(branch, attempt + 1);
}
}

const uploadReportToRemote: StorageAdapter['uploadReportToRemote'] = async (branch, commitSHA, localReport) => {
if (typeof process.env['BUNDLESIZE_ACCOUNT_KEY'] !== 'string') {
throw new Error('monosize-storage-azure: "BUNDLESIZE_ACCOUNT_KEY" is not defined in your process.env');
}

if (typeof process.env['BUNDLESIZE_ACCOUNT_NAME'] !== 'string') {
throw new Error('monosize-storage-azure: "BUNDLESIZE_ACCOUNT_NAME" is not defined in your process.env');
}

const AZURE_STORAGE_ACCOUNT = process.env['BUNDLESIZE_ACCOUNT_NAME'];
const AZURE_STORAGE_TABLE_NAME = 'latest';
const AZURE_ACCOUNT_KEY = process.env['BUNDLESIZE_ACCOUNT_KEY'];

const credentials = new AzureNamedKeyCredential(AZURE_STORAGE_ACCOUNT, AZURE_ACCOUNT_KEY);
const client = new TableClient(
`https://${AZURE_STORAGE_ACCOUNT}.table.core.windows.net`,
AZURE_STORAGE_TABLE_NAME,
credentials,
);

const transaction = new TableTransaction();
const entitiesIterator = await client.listEntities({
queryOptions: {
filter: odata`PartitionKey eq ${branch}`,
},
});

function createRowKey(entry: BundleSizeReportEntry): string {
// Azure does not support slashes in "rowKey"
// https://docs.microsoft.com/archive/blogs/jmstall/azure-storage-naming-rules
return `${entry.packageName}${entry.path.replace(/\.fixture\.js$/, '').replace(/\//g, '')}`;
}

for await (const entity of entitiesIterator) {
// We can't delete and create entries with the same "rowKey" in the same transaction
// => we delete only entries not present in existing report
const isEntryPresentInExistingReport = Boolean(localReport.find(entry => createRowKey(entry) === entity.rowKey));
const shouldEntryBeDeleted = !isEntryPresentInExistingReport;

if (shouldEntryBeDeleted) {
transaction.deleteEntity(entity.partitionKey as string, entity.rowKey as string);
}
}

if (localReport.length === 0) {
console.log([pc.yellow('[w]'), 'No entries to upload'].join(' '));
return;
}

localReport.forEach(entry => {
transaction.upsertEntity(
{
partitionKey: branch,
rowKey: createRowKey(entry),

name: entry.name,
packageName: entry.packageName,
path: entry.path,

minifiedSize: entry.minifiedSize,
gzippedSize: entry.gzippedSize,

commitSHA,
},
'Replace',
);
});

await client.submitTransaction(transaction.actions);
};

return {
getRemoteReport,
getRemoteReport: createGetRemoteReport(config),
uploadReportToRemote,
};
}
Expand Down
3 changes: 3 additions & 0 deletions packages/monosize-storage-azure/src/types.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type AzureStorageConfig = {
endpoint: string;
};
84 changes: 84 additions & 0 deletions packages/monosize-storage-azure/src/uploadReportToRemote.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { AzureNamedKeyCredential, odata, TableClient, TableTransaction } from '@azure/data-tables';
import { BundleSizeReportEntry, StorageAdapter } from 'monosize';
import pc from 'picocolors';

export const ENTRIES_PER_CHUNK = 90;

export function createRowKey(entry: BundleSizeReportEntry): string {
// Azure does not support slashes in "rowKey"
// https://docs.microsoft.com/archive/blogs/jmstall/azure-storage-naming-rules
return `${entry.packageName}${entry.path.replace(/\.fixture\.js$/, '').replace(/\//g, '')}`;
}

export function splitArrayToChunks<T>(arr: T[], size: number): T[][] {
return [...Array(Math.ceil(arr.length / size))].map((_, i) => arr.slice(i * size, (i + 1) * size));
}

export const uploadReportToRemote: StorageAdapter['uploadReportToRemote'] = async (branch, commitSHA, localReport) => {
if (typeof process.env['BUNDLESIZE_ACCOUNT_KEY'] !== 'string') {
throw new Error('monosize-storage-azure: "BUNDLESIZE_ACCOUNT_KEY" is not defined in your process.env');
}

if (typeof process.env['BUNDLESIZE_ACCOUNT_NAME'] !== 'string') {
throw new Error('monosize-storage-azure: "BUNDLESIZE_ACCOUNT_NAME" is not defined in your process.env');
}

if (localReport.length === 0) {
console.log([pc.yellow('[w]'), 'No entries to upload'].join(' '));
return;
}

const AZURE_STORAGE_ACCOUNT = process.env['BUNDLESIZE_ACCOUNT_NAME'];
const AZURE_STORAGE_TABLE_NAME = 'latest';
const AZURE_ACCOUNT_KEY = process.env['BUNDLESIZE_ACCOUNT_KEY'];

const credentials = new AzureNamedKeyCredential(AZURE_STORAGE_ACCOUNT, AZURE_ACCOUNT_KEY);
const client = new TableClient(
`https://${AZURE_STORAGE_ACCOUNT}.table.core.windows.net`,
AZURE_STORAGE_TABLE_NAME,
credentials,
);

const transaction = new TableTransaction();
const entitiesIterator = await client.listEntities({
queryOptions: {
filter: odata`PartitionKey eq ${branch}`,
},
});

for await (const entity of entitiesIterator) {
// We can't delete and create entries with the same "rowKey" in the same transaction
// => we delete only entries not present in existing report
const isEntryPresentInExistingReport = Boolean(localReport.find(entry => createRowKey(entry) === entity.rowKey));
const shouldEntryBeDeleted = !isEntryPresentInExistingReport;

if (shouldEntryBeDeleted) {
transaction.deleteEntity(entity.partitionKey as string, entity.rowKey as string);
}
}

localReport.forEach(entry => {
transaction.upsertEntity(
{
partitionKey: branch,
rowKey: createRowKey(entry),

name: entry.name,
packageName: entry.packageName,
path: entry.path,

minifiedSize: entry.minifiedSize,
gzippedSize: entry.gzippedSize,

commitSHA,
},
'Replace',
);
});

const chunks = splitArrayToChunks(transaction.actions, ENTRIES_PER_CHUNK);

for (const chunk of chunks) {
await client.submitTransaction(chunk);
}
};
Loading

0 comments on commit c0d1761

Please sign in to comment.