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

feat: DVC9059 move core server request logic to new lib #565

Merged
merged 8 commits into from
Oct 24, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { Response } from 'cross-fetch'
import {
DevCycleOptions,
dvcDefaultLogger,
ResponseError,
} from '@devcycle/js-cloud-server-sdk'
import { DVCLogger } from '@devcycle/types'
import { getEnvironmentConfig } from '../src/request'
import { ResponseError } from '@devcycle/server-request'

const setInterval_mock = mocked(setInterval)
const getEnvironmentConfig_mock = mocked(getEnvironmentConfig)
Expand Down
12 changes: 2 additions & 10 deletions lib/shared/config-manager/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DVCLogger } from '@devcycle/types'
// import { UserError } from './utils/userError'
import { getEnvironmentConfig } from './request'
import { ResponseError, DevCycleOptions } from '@devcycle/js-cloud-server-sdk'
import { DevCycleOptions } from '@devcycle/js-cloud-server-sdk'
import { ResponseError, UserError } from '@devcycle/server-request'

type ConfigPollingOptions = DevCycleOptions & {
cdnURI?: string
Expand All @@ -12,14 +12,6 @@ type ClearIntervalInterface = (intervalTimeout: any) => void

type SetConfigBuffer = (sdkKey: string, projectConfig: string) => void

export class UserError extends Error {
constructor(error: Error | string) {
super(error instanceof Error ? error.message : error)
this.name = 'UserError'
this.stack = error instanceof Error ? error.stack : undefined
}
}

export class EnvironmentConfigManager {
private readonly logger: DVCLogger
private readonly sdkKey: string
Expand Down
20 changes: 1 addition & 19 deletions lib/shared/config-manager/src/request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { RequestInitWithRetry } from 'fetch-retry'
import { get } from '@devcycle/js-cloud-server-sdk'
import { getWithTimeout } from '@devcycle/server-request'

export async function getEnvironmentConfig(
url: string,
Expand All @@ -19,20 +18,3 @@ export async function getEnvironmentConfig(
requestTimeout,
)
}

async function getWithTimeout(
url: string,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to common request lib

requestConfig: RequestInit | RequestInitWithRetry,
timeout: number,
): Promise<Response> {
const controller = new AbortController()
const id = setTimeout(() => {
controller.abort()
}, timeout)
const response = await get(url, {
...requestConfig,
signal: controller.signal,
})
clearTimeout(id)
return response
}
18 changes: 18 additions & 0 deletions lib/shared/server-request/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
11 changes: 11 additions & 0 deletions lib/shared/server-request/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# server-request

This library contains common code for making requests to the DevCycle server from server SDKs.

## Building

Run `nx build server-request` to build the library.

## Running unit tests

Run `nx test server-request` to execute the unit tests via [Jest](https://jestjs.io).
6 changes: 6 additions & 0 deletions lib/shared/server-request/__mocks__/cross-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { Request, Response } = jest.requireActual('cross-fetch')

const fetch = jest.fn()

export { Request, Response }
export default fetch
2 changes: 2 additions & 0 deletions lib/shared/server-request/__mocks__/fetch-retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const fetchWithRetry = (_fetch: unknown): unknown => _fetch
export default fetchWithRetry
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ global.fetch = fetch

const fetchRequestMock = fetch as jest.MockedFn<typeof fetch>

import { post, get } from '../src/request'
import { post, get } from '../src/'

describe('request.ts Unit Tests', () => {
beforeEach(() => {
Expand Down
13 changes: 13 additions & 0 deletions lib/shared/server-request/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* eslint-disable */
export default {
displayName: 'server-request',
preset: '../../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/lib/shared/server-request',
}
9 changes: 9 additions & 0 deletions lib/shared/server-request/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@devcycle/server-request",
"version": "1.0.0",
"type": "commonjs",
"private": true,
"dependencies": {
"fetch-retry": "^5.0.3"
}
}
40 changes: 40 additions & 0 deletions lib/shared/server-request/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "server-request",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "lib/shared/server-request/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/lib/shared/server-request",
"main": "lib/shared/server-request/src/index.ts",
"tsConfig": "lib/shared/server-request/tsconfig.lib.json",
"assets": ["lib/shared/server-request/*.md"]
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["lib/shared/server-request/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "lib/shared/server-request/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": []
}
2 changes: 2 additions & 0 deletions lib/shared/server-request/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './request'
export * from './userError'
134 changes: 134 additions & 0 deletions lib/shared/server-request/src/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import fetchWithRetry, { RequestInitWithRetry } from 'fetch-retry'

export class ResponseError extends Error {
constructor(message: string) {
super(message)
this.name = 'ResponseError'
}

status: number
}

const exponentialBackoff: RequestInitWithRetry['retryDelay'] = (attempt) => {
const delay = Math.pow(2, attempt) * 100
const randomSum = delay * 0.2 * Math.random()
return delay + randomSum
}

type retryOnRequestErrorFunc = (
retries: number,
) => RequestInitWithRetry['retryOn']

const retryOnRequestError: retryOnRequestErrorFunc = (retries) => {
return (attempt, error, response) => {
if (attempt >= retries) {
return false
} else if (response && response?.status < 500) {
return false
}

return true
}
}

const handleResponse = async (res: Response) => {
// res.ok only checks for 200-299 status codes
if (!res.ok && res.status >= 400) {
let error
try {
const response: any = await res.clone().json()

Check warning on line 39 in lib/shared/server-request/src/request.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected any. Specify a different type
error = new ResponseError(
response.message || 'Something went wrong',
)
} catch (e) {
error = new ResponseError('Something went wrong')
}
error.status = res.status
throw error
}

return res
}

export async function getWithTimeout(
url: string,
requestConfig: RequestInit | RequestInitWithRetry,
timeout: number,
): Promise<Response> {
const controller = new AbortController()
const id = setTimeout(() => {
controller.abort()
}, timeout)
const response = await get(url, {
...requestConfig,
signal: controller.signal,
})
clearTimeout(id)
return response
}

export async function post(
url: string,
requestConfig: RequestInit | RequestInitWithRetry,
sdkKey: string,
): Promise<Response> {
const [_fetch, config] = await getFetchAndConfig(requestConfig)
const postHeaders = {
...config.headers,
Authorization: sdkKey,
'Content-Type': 'application/json',
}
const res = await _fetch(url, {
...config,
headers: postHeaders,
method: 'POST',
})

return handleResponse(res)
}

export async function get(
url: string,
requestConfig: RequestInit | RequestInitWithRetry,
): Promise<Response> {
const [_fetch, config] = await getFetchAndConfig(requestConfig)
const headers = { ...config.headers, 'Content-Type': 'application/json' }

const res = await _fetch(url, {
...config,
headers,
method: 'GET',
})

return handleResponse(res)
}

async function getFetch() {
if (typeof fetch !== 'undefined') {
return fetch
}

return (await import('cross-fetch')).default
}

async function getFetchWithRetry() {
const fetch = await getFetch()
return fetchWithRetry(fetch)
}

type FetchClient = Awaited<ReturnType<typeof getFetch>>
type FetchAndConfig = [FetchClient, RequestInit]

async function getFetchAndConfig(
requestConfig: RequestInit | RequestInitWithRetry,
): Promise<FetchAndConfig> {
const useRetries = 'retries' in requestConfig
if (useRetries && requestConfig.retries) {
const newConfig: RequestInitWithRetry = { ...requestConfig }
newConfig.retryOn = retryOnRequestError(requestConfig.retries)
newConfig.retryDelay = exponentialBackoff
return [await getFetchWithRetry(), newConfig]
}

return [await getFetch(), requestConfig]
}
13 changes: 13 additions & 0 deletions lib/shared/server-request/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
10 changes: 10 additions & 0 deletions lib/shared/server-request/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": []
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
15 changes: 15 additions & 0 deletions lib/shared/server-request/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "es2019",
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
2 changes: 1 addition & 1 deletion sdk/js-cloud-server/src/cloudClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import {
getAllVariables,
getVariable,
postTrack,
ResponseError,
} from './request'
import { DevCycleUser } from './models/user'
import { ResponseError } from '@devcycle/server-request'

const castIncomingUser = (user: DevCycleUser) => {
if (!(user instanceof DevCycleUser)) {
Expand Down
Loading
Loading