Skip to content

Commit 0737103

Browse files
chore: Improve docs and add circleci config
1 parent ebc96f8 commit 0737103

File tree

6 files changed

+174
-26
lines changed

6 files changed

+174
-26
lines changed

.circleci/config.yml

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
defaults: &defaults
2+
working_directory: ~/dynamic-config
3+
docker:
4+
- image: circleci/node:8.9.4
5+
6+
version: 2
7+
executorType: machine
8+
jobs:
9+
test_node_6:
10+
<<: *defaults
11+
docker:
12+
- image: circleci/node:6.12.3
13+
steps:
14+
- checkout
15+
- run:
16+
name: Install NPM Dependencies
17+
command: npm install
18+
- run:
19+
name: Run Test Suite
20+
command: npm test
21+
22+
test_node_8:
23+
<<: *defaults
24+
steps:
25+
- checkout
26+
- run:
27+
name: Install NPM Dependencies
28+
command: npm install
29+
- run:
30+
name: Run Test Suite
31+
command: npm test
32+
33+
publish:
34+
<<: *defaults
35+
steps:
36+
- checkout
37+
- run:
38+
name: Generate .npmrc File
39+
command: 'echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc'
40+
- run:
41+
name: Install NPM Dependencies
42+
command: npm install
43+
- run:
44+
name: Build Publish Assets
45+
command: npm run build
46+
- run:
47+
name: Publish to NPM
48+
command: npm publish --access public
49+
50+
publish_next:
51+
<<: *defaults
52+
steps:
53+
- checkout
54+
- run:
55+
name: Generate .npmrc File
56+
command: 'echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc'
57+
- run:
58+
name: Install NPM Dependencies
59+
command: npm install
60+
- run:
61+
name: Build Publish Assets
62+
command: npm run build
63+
- run:
64+
name: Publish to NPM
65+
command: npm publish --tag next --access public
66+
67+
workflows:
68+
version: 2
69+
build_publish:
70+
jobs:
71+
- test_node_6:
72+
filters:
73+
tags:
74+
only: /.*/
75+
76+
- test_node_8:
77+
filters:
78+
tags:
79+
only: /.*/
80+
81+
- publish:
82+
requires:
83+
- test_node_6
84+
- test_node_8
85+
filters:
86+
tags:
87+
only: /^(v){1}[0-9]+(\.[0-9]+){2}$/
88+
branches:
89+
ignore: /.*/
90+
91+
- publish_next:
92+
requires:
93+
- test_node_6
94+
- test_node_8
95+
filters:
96+
tags:
97+
only: /^(v){1}[0-9]+(\.[0-9]+){2}(-)[0-9]+$/
98+
branches:
99+
ignore: /.*/

README.md

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ A dynamic feature flags library for Node.js. This library gives you fine-grained
44

55
This implementation is largely based on [Feature Toggles](https://twitter.github.io/finagle/guide/Configuration.html#feature-toggles) in [Twitter's Finagle framework](https://github.com/twitter/finagle).
66

7+
For a detail looks at feature flags check out this article [Feature Toggles](https://martinfowler.com/articles/feature-toggles.html).
8+
9+
The idea behind feature flags is that they give you a way for testing new code and ramping that code over time. In your configuration you would provide a description of what percentage of requests should use a particular code path and the library will somewhat randomly return a boolean for each toggle representing if it should be used on a particular request or not. Once a code path is ramped to 100% it is expected the feature flag for that code would be removed.
10+
711
## Install
812

913
This feature flags library has a peer dependency on `@creditkarma/dynamic-config`.
@@ -15,9 +19,51 @@ $ npm install --save @creditkarma/feature-flags
1519

1620
## Usage
1721

22+
Using feature flags is very easy. They are really just a boolean indicating which code path you should use.
23+
24+
```typescript
25+
import { toggleMap, Toggle } from '@creditkarma/feature-flags'
26+
27+
async function startApp(app) {
28+
const toggle: Toggle = await toggleMap('com.example.service.UseNewBackend')
29+
30+
app.get('/test', (req, res) => {
31+
if (toggle()) {
32+
// Use new code path
33+
} else {
34+
// Use old code path
35+
}
36+
})
37+
}
38+
```
39+
40+
In application code there are only two things you need to use. The `toggleMap` function and the `Toggle` type.
41+
42+
### `toggleMap`
43+
44+
The `toggleMap` is a function that returns a Promise of a toggle for a given key. Your application can have many toggles. Each of these toggles has a unique string identifier that you specify. You provide this id to the `toggleMap` function and it gives you back a `Promise<Toggle>`. A `Toggle` is a function that just returns a `boolean`. It will return, randomly, `true` based on the configured percentage.
45+
46+
## Configuration
47+
48+
The real work of the feature flag is actually the configuration. In your application config you would have a key call `toggles`. This key is expected to be at the root level of your config `JSON`:
49+
50+
```json
51+
{
52+
"toggles": [
53+
{
54+
"id": "com.example.service.UseNewBackend",
55+
"description": "Use new backend code",
56+
"fraction": 0.1,
57+
}
58+
]
59+
}
60+
```
61+
1862
### Dynamic Config
1963

20-
[DynamicConfig](https://github.com/creditkarma/dynamic-config) is a pluggable config library for Node.js that allows for runtime changes to config values.
64+
[DynamicConfig](https://github.com/creditkarma/dynamic-config) is a pluggable config library for Node.js that allows for runtime changes to config values. This is the basis for our feature flags support. It certainly isn't required that you use Dyanmic Config for all of your application config, but that would be recommended.
65+
66+
2167

2268
### Flag Schema
2369

@@ -53,15 +99,3 @@ $ npm install --save @creditkarma/feature-flags
5399
"required": [ "toggles" ]
54100
}
55101
```
56-
57-
```json
58-
{
59-
"toggles": [
60-
{
61-
"id": "com.example.service.UseNewBackend",
62-
"description": "Use new backend code",
63-
"fraction": 0.1,
64-
}
65-
]
66-
}
67-
```

src/main/toggles.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { config } from '@creditkarma/dynamic-config'
22
import * as logger from './logger'
33
import { DEFAULT_TOGGLES_PATH } from './constants'
4-
import { objectMatchesSchema } from './utils'
4+
import { objectMatchesSchema, memoize } from './utils'
55
import { toggleSchema } from './schema'
66
import {
7-
DescMap,
7+
ToggleMap,
88
IToggleDescription,
99
Toggle,
1010
} from './types'
1111

12-
const rawToggles: DescMap = new Map()
12+
const rawToggles: ToggleMap = new Map()
1313

14-
const futureToggles: Promise<DescMap> =
15-
new Promise((resolve, reject) => {
14+
const lazyToggles: () => Promise<ToggleMap> = memoize(() => {
15+
return new Promise((resolve, reject) => {
1616
config().watch<Array<IToggleDescription>>(DEFAULT_TOGGLES_PATH).onValue((toggles): void => {
1717
if (objectMatchesSchema(toggleSchema, { toggles })) {
1818
toggles.forEach((next: IToggleDescription) => {
@@ -27,12 +27,13 @@ const futureToggles: Promise<DescMap> =
2727
}
2828
})
2929
})
30+
})
3031

31-
export function ToggleMap(key: string): Promise<Toggle> {
32-
return futureToggles.then((map: DescMap) => {
32+
export function toggleMap(key: string): Promise<Toggle> {
33+
return lazyToggles().then((map: ToggleMap) => {
3334
return () => {
34-
if (rawToggles.has(key)) {
35-
const toggleDesc: IToggleDescription = rawToggles.get(key)!
35+
if (map.has(key)) {
36+
const toggleDesc: IToggleDescription = map.get(key)!
3637
const randomInt: number = Math.random()
3738
return (randomInt < toggleDesc.fraction)
3839

src/main/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type DescMap = Map<string, IToggleDescription>
1+
export type ToggleMap = Map<string, IToggleDescription>
22

33
export interface IToggleDescription {
44
id: string

src/main/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,18 @@ const JSON_VALIDATOR: JsonValidator.Ajv = new JsonValidator()
44

55
export function objectMatchesSchema(schema: object, data: any): boolean {
66
return JSON_VALIDATOR.validate(schema, data) as boolean
7+
}
8+
9+
export function memoize<T>(fn: () => T): () => T {
10+
let cachedValue: any = undefined
11+
12+
return (): T => {
13+
if (cachedValue !== undefined) {
14+
return cachedValue
15+
16+
} else {
17+
cachedValue = fn()
18+
return cachedValue
19+
}
20+
}
721
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect } from 'code'
22
import * as Lab from 'lab'
33

4-
import { ToggleMap } from '../../main'
4+
import { toggleMap } from '../../main'
55

66
export const lab = Lab.script()
77

@@ -10,12 +10,12 @@ const it = lab.it
1010

1111
describe('ToggleMap', () => {
1212
it('should return false for toggle set to 0.0', async () => {
13-
const toggle = await ToggleMap('com.creditkarma.featureFlags.AlwaysDisabled')
13+
const toggle = await toggleMap('com.creditkarma.featureFlags.AlwaysDisabled')
1414
expect(toggle()).to.equal(false)
1515
})
1616

1717
it('should return true for toggle set to 1.0', async () => {
18-
const toggle = await ToggleMap('com.creditkarma.featureFlags.AlwaysEnabled')
18+
const toggle = await toggleMap('com.creditkarma.featureFlags.AlwaysEnabled')
1919
expect(toggle()).to.equal(true)
2020
})
2121
})

0 commit comments

Comments
 (0)