Skip to content

Commit

Permalink
initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
menduz committed Aug 20, 2021
1 parent e12d1f2 commit e8f3143
Show file tree
Hide file tree
Showing 6 changed files with 406 additions and 19 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ LOCAL_ARG = --local --verbose --diagnostics
endif

test:
node_modules/.bin/jest --detectOpenHandles --colors --runInBand $(TESTARGS)
node_modules/.bin/jest --detectOpenHandles --colors --runInBand --coverage $(TESTARGS)

test-watch:
node_modules/.bin/jest --detectOpenHandles --colors --runInBand --watch $(TESTARGS)
Expand Down
49 changes: 49 additions & 0 deletions etc/rollouts-lib.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## API Report File for "@well-known-components/rollouts-lib"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

// @public
export function calculateRollout(context: Context, records: RolloutRecord[]): RolloutRecord;

// @public
export function calculateRolloutsForDomain(context: Context, domain: Partial<RolloutDomain>): Record<string, RolloutRecord>;

// @public
export type Context = {
userId?: string;
sessionId?: string;
remoteAddress?: string;
};

// @public
export function normalizedValue(context: Context, rollout: RolloutRecord): number;

// @public (undocumented)
export function patchRollouts(currentValues: Partial<RolloutDomain>, rolloutName: string, patch: Pick<RolloutRecord, "percentage" | "prefix" | "version">, timestamp?: number): Partial<RolloutDomain>;

// @public (undocumented)
export type RolloutDomain = {
records: Record<RolloutName, RolloutRecord[]>;
};

// @public (undocumented)
export type RolloutName = string;

// @public (undocumented)
export type RolloutRecord = {
version: string;
percentage: number;
prefix: string;
updatedAt?: number;
createdAt?: number;
};

// @public (undocumented)
export type RolloutVersion = string;


// (No @packageDocumentation comment for this package)

```
20 changes: 16 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 12 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
{
"name": "@well-known-components/base-component",
"name": "@well-known-components/rollouts-lib",
"version": "1.0.0",
"description": "base component",
"description": "Progressive rollouts helpers",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {},

"repository": {
"type": "git",
"url": "git+https://github.com/well-known-components/base-component.git"
"url": "git+https://github.com/well-known-components/rollouts-lib.git"
},
"keywords": [],
"author": "",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/well-known-components/base-component/issues"
"url": "https://github.com/well-known-components/rollouts-lib/issues"
},
"prettier": {
"printWidth": 120,
"semi": false
},
"homepage": "https://github.com/well-known-components/base-component#readme",
"homepage": "https://github.com/well-known-components/rollouts-lib#readme",
"devDependencies": {
"@microsoft/api-extractor": "^7.17.0",
"typescript": "^4.3.5",
"@types/jest": "^26.0.23",
"jest": "^27.0.6",
"ts-jest": "^27.0.3"
},
"dependencies": {},
"dependencies": {
"@types/murmurhash3js": "^3.0.2",
"@types/semver": "^7.3.8",
"murmurhash3js": "^3.0.1",
"semver": "^7.3.5"
},
"files": [
"dist"
]
Expand Down
149 changes: 145 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,148 @@
import * as murmurHash3 from "murmurhash3js"
import { compare, valid } from "semver"

/**
* A function that does something
* @public
*/
export function example(){
return true
}
export type RolloutName = string

/**
* @public
*/
export type RolloutVersion = string

/**
* @public
*/
export type RolloutDomain = {
records: Record<RolloutName, RolloutRecord[]>
}

/**
* @public
*/
export type RolloutRecord = {
version: string
percentage: number
prefix: string

updatedAt?: number
createdAt?: number
}

/**
* User context used to calculate rollouts.
* @public
*/
export type Context = {
userId?: string
sessionId?: string
remoteAddress?: string
}

/**
* This function calculates a normalized value for a RolloutRecord and a Context.
* The function is stable and deterministic for non-empty contexts,
* it returns a numeric hash from 1 to 100.
* @public
*/
export function normalizedValue(context: Context, rollout: RolloutRecord): number {
const userId = context.userId || context.sessionId || context.remoteAddress || Math.random()
const rolloutId = `${rollout.version}:${rollout.prefix}`
return (murmurHash3.x86.hash32(`${rolloutId}:${userId}`) % 100) + 1
}

/**
* Calculates and selects a rollout for a list of RolloutRecords and a given
* Context.
*
* @public
*/
export function calculateRollout(context: Context, records: RolloutRecord[]): RolloutRecord {
if (records.length == 0) {
throw new Error("Empty rollouts")
}
for (let rollout of records) {
const normalizedUserId = normalizedValue(context, rollout)
if (rollout.percentage > 0 && normalizedUserId <= rollout.percentage) {
return rollout
}
}
// return last as fallback
return records[records.length - 1]
}

/**
* Calculates all rollouts for a specific RolloutDomain and Context
*
* @public
*/
export function calculateRolloutsForDomain(
context: Context,
domain: Partial<RolloutDomain>
): Record<string, RolloutRecord> {
const map: Record<string, RolloutRecord> = {}

if (domain.records) {
for (let rolloutName of Object.keys(domain.records)) {
if (domain.records[rolloutName] && domain.records[rolloutName].length) {
map[rolloutName] = calculateRollout(context, domain.records[rolloutName])
}
}
}

return map
}

/**
* @public
*/
export function patchRollouts(
currentValues: Partial<RolloutDomain>,
rolloutName: string,
patch: Pick<RolloutRecord, "percentage" | "prefix" | "version">,
timestamp: number = Date.now()
): Partial<RolloutDomain> {
const { version, percentage, prefix } = patch

if (typeof rolloutName != "string" || !rolloutName.length) {
throw new Error("patchRollouts: invalid rolloutName")
}
if (typeof version != "string" || !version.length || !valid(version)) {
throw new Error("patchRollouts: invalid version: " + version)
}
if (typeof percentage != "number" || isNaN(percentage) || percentage < 0 || percentage > 100) {
throw new Error(`patchRollouts: invalid percentage ${percentage}`)
}
if (typeof timestamp != "number" || isNaN(timestamp) || timestamp < 0) {
throw new Error(`patchRollouts: invalid timestamp ${timestamp}`)
}

// always normalize for backwards compatibility
currentValues.records = currentValues.records || {}
currentValues.records[rolloutName] = currentValues.records[rolloutName] || []

// apply changes
let currentVersion = currentValues.records[rolloutName]!.find(($) => $.version == version)

if (!currentVersion) {
currentVersion = {
percentage: percentage | 0,
prefix,
version,
createdAt: timestamp,
}
currentValues.records[rolloutName]!.push(currentVersion)
}

// override currentVersion percentage
currentVersion.percentage = percentage
currentVersion.updatedAt = timestamp

// sort deployments before saving
currentValues.records[rolloutName]!.sort((a, b) => {
return -compare(a.version, b.version)
})

return currentValues
}
Loading

0 comments on commit e8f3143

Please sign in to comment.