-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #21 from hirosystems/master
merge master into develop
- Loading branch information
Showing
11 changed files
with
271 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { FastifyReply } from 'fastify'; | ||
import { logger } from '../logger'; | ||
|
||
/** | ||
* A `Cache-Control` header used for re-validation based caching. | ||
* * `public` == allow proxies/CDNs to cache as opposed to only local browsers. | ||
* * `no-cache` == clients can cache a resource but should revalidate each time before using it. | ||
* * `must-revalidate` == somewhat redundant directive to assert that cache must be revalidated, required by some CDNs | ||
*/ | ||
export const CACHE_CONTROL_MUST_REVALIDATE = 'public, no-cache, must-revalidate'; | ||
|
||
export async function setResponseNonCacheable(reply: FastifyReply) { | ||
await reply.removeHeader('Cache-Control'); | ||
await reply.removeHeader('ETag'); | ||
} | ||
|
||
/** | ||
* Parses the etag values from a raw `If-None-Match` request header value. | ||
* The wrapping double quotes (if any) and validation prefix (if any) are stripped. | ||
* The parsing is permissive to account for commonly non-spec-compliant clients, proxies, CDNs, etc. | ||
* E.g. the value: | ||
* ```js | ||
* `"a", W/"b", c,d, "e", "f"` | ||
* ``` | ||
* Would be parsed and returned as: | ||
* ```js | ||
* ['a', 'b', 'c', 'd', 'e', 'f'] | ||
* ``` | ||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#syntax | ||
* ``` | ||
* If-None-Match: "etag_value" | ||
* If-None-Match: "etag_value", "etag_value", ... | ||
* If-None-Match: * | ||
* ``` | ||
* @param ifNoneMatchHeaderValue - raw header value | ||
* @returns an array of etag values | ||
*/ | ||
export function parseIfNoneMatchHeader( | ||
ifNoneMatchHeaderValue: string | undefined | ||
): string[] | undefined { | ||
if (!ifNoneMatchHeaderValue) { | ||
return undefined; | ||
} | ||
// Strip wrapping double quotes like `"hello"` and the ETag validation-prefix like `W/"hello"`. | ||
// The API returns compliant, strong-validation ETags (double quoted ASCII), but can't control what | ||
// clients, proxies, CDNs, etc may provide. | ||
const normalized = /^(?:"|W\/")?(.*?)"?$/gi.exec(ifNoneMatchHeaderValue.trim())?.[1]; | ||
if (!normalized) { | ||
// This should never happen unless handling a buggy request with something like `If-None-Match: ""`, | ||
// or if there's a flaw in the above code. Log warning for now. | ||
logger.warn(`Normalized If-None-Match header is falsy: ${ifNoneMatchHeaderValue}`); | ||
return undefined; | ||
} else if (normalized.includes(',')) { | ||
// Multiple etag values provided, likely irrelevant extra values added by a proxy/CDN. | ||
// Split on comma, also stripping quotes, weak-validation prefixes, and extra whitespace. | ||
return normalized.split(/(?:W\/"|")?(?:\s*),(?:\s*)(?:W\/"|")?/gi); | ||
} else { | ||
// Single value provided (the typical case) | ||
return [normalized]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
export * from './cache'; | ||
export * from './fastify'; | ||
export * from './openapi'; | ||
export * from './schemas'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { TSchema, Type } from '@sinclair/typebox'; | ||
|
||
export const Nullable = <T extends TSchema>(type: T) => Type.Union([type, Type.Null()]); | ||
export const Optional = <T extends TSchema>(type: T) => Type.Optional(type); | ||
|
||
export const PaginatedResponse = <T extends TSchema>(type: T, title: string) => | ||
Type.Object( | ||
{ | ||
limit: Type.Integer({ examples: [20] }), | ||
offset: Type.Integer({ examples: [0] }), | ||
total: Type.Integer({ examples: [1] }), | ||
results: Type.Array(type), | ||
}, | ||
{ title } | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './iterators'; | ||
export * from './time'; | ||
export * from './values'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { logger } from '../logger'; | ||
import { isDevEnv } from './values'; | ||
|
||
/** | ||
* Iterate over an array, yielding multiple items at a time. If the size of the given array | ||
* is not divisible by the given batch size, then the length of the last items returned will | ||
* be smaller than the given batch size, i.e.: | ||
* ```typescript | ||
* items.length % batchSize | ||
* ``` | ||
* @param items - The array to iterate over. | ||
* @param batchSize - Maximum number of items to return at a time. | ||
* @param printBenchmark - If we should print benchmark of items per second | ||
*/ | ||
export function* batchIterate<T>( | ||
items: T[], | ||
batchSize: number, | ||
printBenchmark = isDevEnv | ||
): Generator<T[]> { | ||
if (items.length === 0) return; | ||
const startTime = Date.now(); | ||
for (let i = 0; i < items.length; ) { | ||
const itemsRemaining = items.length - i; | ||
const sliceSize = Math.min(batchSize, itemsRemaining); | ||
yield items.slice(i, i + sliceSize); | ||
i += sliceSize; | ||
} | ||
if (printBenchmark) { | ||
const itemsPerSecond = Math.round((items.length / (Date.now() - startTime)) * 1000); | ||
const caller = new Error().stack?.split('at ')[3].trim(); | ||
logger.debug(`Iterated ${itemsPerSecond} items/second at ${caller}`); | ||
} | ||
} | ||
|
||
/** | ||
* Iterate over an `AsyncIterable`, yielding multiple items at a time. If the size of the given | ||
* array is not divisible by the given batch size, then the length of the last items returned will | ||
* be smaller than the given batch size. | ||
* | ||
* @param items - AsyncIterable | ||
* @param batchSize - Batch size | ||
* @param printBenchmark - If we should print benchmark of items per second | ||
*/ | ||
export async function* asyncBatchIterate<T>( | ||
items: AsyncIterable<T>, | ||
batchSize: number, | ||
printBenchmark = isDevEnv | ||
): AsyncGenerator<T[], void, unknown> { | ||
const startTime = Date.now(); | ||
let itemCount = 0; | ||
let itemBatch: T[] = []; | ||
for await (const item of items) { | ||
itemBatch.push(item); | ||
itemCount++; | ||
if (itemBatch.length >= batchSize) { | ||
yield itemBatch; | ||
itemBatch = []; | ||
if (printBenchmark) { | ||
const itemsPerSecond = Math.round((itemCount / (Date.now() - startTime)) * 1000); | ||
const caller = new Error().stack?.split('at ')[3].trim(); | ||
logger.debug(`Iterated ${itemsPerSecond} items/second at ${caller}`); | ||
} | ||
} | ||
} | ||
if (itemBatch.length > 0) { | ||
yield itemBatch; | ||
} | ||
} | ||
|
||
/** | ||
* Convert an `AsyncIterable` to a generator | ||
* @param iter - AsyncIterable | ||
*/ | ||
export async function* asyncIterableToGenerator<T>(iter: AsyncIterable<T>) { | ||
for await (const entry of iter) { | ||
yield entry; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.