Skip to content

Commit

Permalink
Merge branch 'master' into errors
Browse files Browse the repository at this point in the history
  • Loading branch information
andrepolischuk committed Aug 15, 2024
2 parents 3bfbcd5 + 2145de6 commit 89f2894
Show file tree
Hide file tree
Showing 70 changed files with 3,787 additions and 9 deletions.
28 changes: 28 additions & 0 deletions .size-limit.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
[
{
"path": "packages/async/dist/index.js",
"limit": "400 B"
},
{
"path": "packages/cookie-storage/dist/index.js",
"limit": "990 B"
},
{
"path": "packages/crypto/dist/index.js",
"limit": "27.3 KB"
},
{
"path": "packages/debug/dist/index.js",
"limit": "3.4 KB"
},
{
"path": "packages/dom/dist/index.js",
"limit": "500 B"
},
{
"path": "packages/errors/dist/index.js",
"limit": "290 B"
Expand All @@ -11,8 +27,20 @@
"path": "packages/local-storage/dist/index.js",
"limit": "290 B"
},
{
"path": "packages/react/dist/index.js",
"limit": "4.25 KB"
},
{
"path": "packages/session-storage/dist/index.js",
"limit": "290 B"
},
{
"path": "packages/splits/dist/index.js",
"limit": "1.6 KB"
},
{
"path": "packages/url/dist/index.js",
"limit": "675 B"
}
]
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ Common utils used by Rambler team

## Packages

- [@rambler-tech/async](packages/async)
- [@rambler-tech/cookie-storage](packages/cookie-storage)
- [@rambler-tech/crypto](packages/crypto)
- [@rambler-tech/debug](packages/debug)
- [@rambler-tech/dom](packages/dom)
- [@rambler-tech/errors](packages/errors)
- [@rambler-tech/local-storage](packages/local-storage)
- [@rambler-tech/session-storage](packages/session-storage)
- [@rambler-tech/lhci-report](packages/lhci-report)
- [@rambler-tech/local-storage](packages/local-storage)
- [@rambler-tech/react](packages/react)
- [@rambler-tech/session-storage](packages/session-storage)
- [@rambler-tech/splits](packages/splits)
- [@rambler-tech/url](packages/url)

## Contributing

Expand Down
5 changes: 4 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ module.exports = {
moduleDirectories: ['packages', 'node_modules'],
collectCoverage: true,
coverageReporters: ['text'],
setupFilesAfterEnv: ['./jest.setup.js']
setupFilesAfterEnv: ['./jest.setup.js'],
testEnvironmentOptions: {
url: 'https://example.com/'
}
}
10 changes: 10 additions & 0 deletions packages/async/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# 0.1.0 (2024-08-14)

### Features

- **async:** add async tools ([946d5ba](https://github.com/rambler-digital-solutions/rambler-common/commit/946d5baf89b77fa07f9845ef68e3d8f5b6d7dd5f))
15 changes: 15 additions & 0 deletions packages/async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Async

Async tools

## Install

```
npm install -D @rambler-tech/async
```

or

```
yarn add -D @rambler-tech/async
```
88 changes: 88 additions & 0 deletions packages/async/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {retry, wait} from '.'

test('retry resolved promise', async () => {
const fn = jest.fn((...args) => Promise.resolve(args))
const args = [1, 2, 3]
const result = await retry(fn)(...args)

expect(result).toEqual(args)
expect(fn).toHaveBeenCalledTimes(1)
})

test('retry rejected and last resolved promise', async () => {
let counter = 0
const fn = jest.fn((...args) =>
counter++ > 1 ? Promise.resolve(args) : Promise.reject(new Error('error'))
)
const args = [1, 2, 3]
const result = await retry(fn, {retries: 3, timeout: 10})(...args)

expect(result).toEqual(args)
expect(fn).toHaveBeenCalledTimes(3)
})

test('retry rejected and ignore rejected with specific error', async () => {
let counter = 0
const fn = jest.fn(() =>
Promise.reject(
new Error(counter++ > 0 ? 'aborted by timeout' : 'yet another error')
)
)

const error = await retry(fn, {
retries: 3,
timeout: 10,
shouldRetry: (error) => !error.toString().match(/aborted/)
})().catch((error) => error)

expect(error.message).toBe('aborted by timeout')
expect(fn).toHaveBeenCalledTimes(2)
})

test('retry rejected promise', async () => {
const fn = jest.fn(() => Promise.reject(new Error('failed')))
const error = await retry(fn, {retries: 3, timeout: 10})().catch(
(error) => error
)

expect(error.message).toBe('failed')
expect(fn).toHaveBeenCalledTimes(3)
})

test('abort retry', async () => {
const abortController = new AbortController()
const fn = jest.fn(() => Promise.reject(new Error('failed')))
const promise = retry(fn, {
retries: 3,
timeout: 10,
signal: abortController.signal
})()

// NOTE: simulate microtask to abort wait after first attempt
await Promise.resolve().then(() => abortController.abort())

const error = await promise.catch((error) => error)

expect(error.message).toBe('The user aborted a timeout.')
expect(fn).toHaveBeenCalledTimes(1)
})

test('wait timeout', () => {
jest.useFakeTimers()
jest.spyOn(global, 'setTimeout')

const promise = wait(1000)

jest.advanceTimersByTime(1000)

expect(promise).resolves.toBeUndefined()
})

test('aborted wait timeout', () => {
const abortController = new AbortController()
const promise = wait(1000, abortController.signal)

abortController.abort()

expect(promise).rejects.toThrow('The user aborted a timeout.')
})
58 changes: 58 additions & 0 deletions packages/async/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* eslint-disable import/no-unused-modules */

const RETRIES_LEFT = 3
const INTERVAL = 500

export type PromiseFactory<T> = (...args: any[]) => Promise<T>

/** Retry options */
export interface RetryOptions {
/** Maximum amount of times to retry the operation, default is 3 */
retries?: number
/** Number of milliseconds before starting the retry, default is 500 */
timeout?: number
/** Check an error to need retry, by default retry on every error */
shouldRetry?: (error: Error) => boolean
/** AbortSignal instance to abort retry via an AbortController */
signal?: AbortSignal
}

/** Retry function call */
export function retry<T>(
factory: PromiseFactory<T>,
options: RetryOptions = {}
): PromiseFactory<T> {
let {retries = RETRIES_LEFT} = options
const {timeout = INTERVAL, shouldRetry = () => true, signal} = options

async function call(...args: any[]): Promise<T> {
try {
return await factory(...args)
} catch (error: any) {
if (--retries < 1 || !shouldRetry(error)) {
throw error
}

await wait(timeout, signal)

return call(...args)
}
}

return (...args: any[]): Promise<T> => call(...args)
}

/** Wait function call */
export function wait(timeout: number, signal?: AbortSignal): Promise<void> {
return new Promise<void>((resolve, reject) => {
const timeoutId = window.setTimeout(resolve, timeout)

signal?.addEventListener('abort', () => {
if (timeoutId) {
clearTimeout(timeoutId)
}

reject(new DOMException('The user aborted a timeout.', 'AbortError'))
})
})
}
12 changes: 12 additions & 0 deletions packages/async/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@rambler-tech/async",
"version": "0.1.0",
"main": "dist",
"module": "dist",
"types": "dist/index.d.ts",
"license": "MIT",
"sideEffects": false,
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions packages/async/tsconfig.json
1 change: 1 addition & 0 deletions packages/async/typedoc.json
10 changes: 10 additions & 0 deletions packages/crypto/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# 0.1.0 (2024-08-14)

### Features

- **crypto:** add crypto utils ([e6543a2](https://github.com/rambler-digital-solutions/rambler-common/commit/e6543a2d9b70b4b9d4ff4c19250c32aee2161c37))
15 changes: 15 additions & 0 deletions packages/crypto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Crypto

Browser crypto utils

## Install

```
npm install -D @rambler-tech/crypto
```

or

```
yarn add -D @rambler-tech/crypto
```
72 changes: 72 additions & 0 deletions packages/crypto/aesrsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {getRandomValues, subtle} from './crypto'
import {
bufferFromString,
bufferFromUnicode,
stringFromBuffer,
base64urlFromString
} from './buffers'

function generateAESKey() {
return subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true,
['encrypt', 'decrypt']
)
}

async function encryptAES(
key: CryptoKey,
initVector: Uint8Array,
body: string
) {
const encryptedBody = await subtle.encrypt(
{name: 'AES-GCM', iv: initVector},
key,
bufferFromUnicode(body)
)

return base64urlFromString(stringFromBuffer(encryptedBody))
}

function importRSAKey(keyString: string) {
return subtle.importKey(
'spki',
bufferFromString(window.atob(keyString)),
{
name: 'RSA-OAEP',
hash: {name: 'SHA-256'}
},
false,
['wrapKey']
)
}

async function encryptRSA(key: CryptoKey, body: CryptoKey) {
const encryptedBody = await subtle.wrapKey('raw', body, key, {
name: 'RSA-OAEP'
} as RsaOaepParams)

return base64urlFromString(stringFromBuffer(encryptedBody))
}

/** Encrypt with AES and RSA keys */
export async function encryptAESRSA(keyString: string, body: string) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const initVector = getRandomValues(new Uint8Array(12))
const initVectorString = base64urlFromString(stringFromBuffer(initVector))

const [aesKey, rsaKey] = await Promise.all([
generateAESKey(),
importRSAKey(keyString)
])

const [encryptedBody, encryptedKey] = await Promise.all([
encryptAES(aesKey, initVector, body),
encryptRSA(rsaKey, aesKey)
])

return `${encryptedBody}.${encryptedKey}.${initVectorString}`
}
Loading

0 comments on commit 89f2894

Please sign in to comment.