Skip to content
2 changes: 1 addition & 1 deletion packages/sdk/cloudflare/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"module": "./dist/index.mjs",
"packageManager": "yarn@3.4.1",
"dependencies": {
"@launchdarkly/cloudflare-server-sdk": "0.0.1"
"@launchdarkly/cloudflare-server-sdk": "2.0.1"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230321.0",
Expand Down
9 changes: 5 additions & 4 deletions packages/sdk/cloudflare/example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
async fetch(request: Request, env: Bindings): Promise<Response> {
const sdkKey = 'test-sdk-key';
const flagKey = 'testFlag1';
const context = { kind: 'user', key: 'test-user-key-1' };
const context = { kind: 'user', key: 'test-user-key-1', email: 'test@gmail.com' };

// start using ld
const client = initLD(sdkKey, env.LD_KV);
Expand All @@ -13,9 +13,10 @@ export default {
const flagDetail = await client.variationDetail(flagKey, context, false);
const allFlags = await client.allFlagsState(context);

const resp = `${flagKey}: ${flagValue}, detail: ${JSON.stringify(
flagDetail
)}, allFlags: ${JSON.stringify(allFlags)}`;
const resp = `
${flagKey}: ${flagValue}
detail: ${JSON.stringify(flagDetail)}
allFlags: ${JSON.stringify(allFlags)}`;

// eslint-disable-next-line
console.log(`------------- ${resp}`);
Expand Down
79 changes: 76 additions & 3 deletions packages/sdk/cloudflare/example/src/testData.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,26 @@
"on": true,
"prerequisites": [],
"targets": [],
"rules": [],
"rules": [
{
"variation": 1,
"id": "rule1",
"clauses": [
{
"contextKind": "user",
"attribute": "/email",
"op": "contains",
"values": ["gmail"],
"negate": false
}
],
"trackEvents": false,
"rollout": {
"bucketBy": "bucket",
"variations": [{ "variation": 1, "weight": 100 }]
}
}
],
"fallthrough": {
"variation": 0
},
Expand All @@ -25,10 +44,52 @@
},
"testFlag2": {
"key": "testFlag2",
"on": false,
"on": true,
"prerequisites": [],
"targets": [],
"rules": [],
"fallthrough": {
"variation": 0,
"rollout": {
"bucketBy": "bucket",
"variations": [{ "variation": 1, "weight": 100 }],
"contextKind:": "user",
"attribute": "/email"
}
},
"offVariation": 1,
"variations": [true, false],
"clientSideAvailability": {
"usingMobileKey": true,
"usingEnvironmentId": true
},
"clientSide": true,
"salt": "aef830243d6640d0a973be89988e008d",
"trackEvents": false,
"trackEventsFallthrough": false,
"debugEventsUntilDate": null,
"version": 2,
"deleted": false
},
"testFlag3": {
"key": "testFlag3",
"on": true,
"prerequisites": [],
"targets": [],
"rules": [
{
"variation": 1,
"id": "rule1",
"clauses": [
{
"op": "segmentMatch",
"values": ["testSegment1"],
"negate": false
}
],
"trackEvents": false
}
],
"fallthrough": {
"variation": 0
},
Expand Down Expand Up @@ -65,7 +126,19 @@
},
"site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" }
},
"rules": [],
"rules": [
{
"id": "rule-country",
"clauses": [
{
"attribute": "country",
"op": "in",
"values": ["australia"],
"negate": false
}
]
}
],
"version": 1,
"deleted": false,
"_access": { "denied": [], "allowed": [] },
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"start": "rimraf dist && yarn tsw",
"lint": "eslint . --ext .ts",
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore",
"test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" jest --ci --runInBand --coverage",
"test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" jest --ci --runInBand",
"coverage": "yarn test --coverage",
"check": "yarn prettier && yarn lint && yarn build && yarn test && yarn doc"
},
"dependencies": {
Expand Down
22 changes: 11 additions & 11 deletions packages/sdk/cloudflare/src/createFeatureStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('createFeatureStore', () => {
let asyncFeatureStore: AsyncStoreFacade;

beforeEach(() => {
mockGet.mockImplementation(() => Promise.resolve(testData));
mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
featureStore = createFeatureStore(mockKV, sdkKey, mockLogger);
asyncFeatureStore = new AsyncStoreFacade(featureStore);
});
Expand All @@ -31,8 +31,8 @@ describe('createFeatureStore', () => {
test('get flag', async () => {
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');

expect(mockGet).toHaveBeenCalledWith(kvKey, { type: 'json' });
expect(flag).toEqual(testData.flags.testFlag1);
expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(flag).toMatchObject(testData.flags.testFlag1);
});

test('invalid flag key', async () => {
Expand All @@ -44,8 +44,8 @@ describe('createFeatureStore', () => {
test('get segment', async () => {
const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'testSegment1');

expect(mockGet).toHaveBeenCalledWith(kvKey, { type: 'json' });
expect(segment).toEqual(testData.segments.testSegment1);
expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(segment).toMatchObject(testData.segments.testSegment1);
});

test('invalid segment key', async () => {
Expand All @@ -64,23 +64,23 @@ describe('createFeatureStore', () => {

describe('all', () => {
test('all flags', async () => {
const flag = await asyncFeatureStore.all({ namespace: 'features' });
const flags = await asyncFeatureStore.all({ namespace: 'features' });

expect(mockGet).toHaveBeenCalledWith(kvKey, { type: 'json' });
expect(flag).toEqual(testData.flags);
expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(flags).toMatchObject(testData.flags);
});

test('all segments', async () => {
const segment = await asyncFeatureStore.all({ namespace: 'segments' });

expect(mockGet).toHaveBeenCalledWith(kvKey, { type: 'json' });
expect(segment).toEqual(testData.segments);
expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(segment).toMatchObject(testData.segments);
});

test('invalid DataKind', async () => {
const flag = await asyncFeatureStore.all({ namespace: 'InvalidDataKind' });

expect(flag).toBeUndefined();
expect(flag).toEqual({});
});

test('invalid kv key', async () => {
Expand Down
62 changes: 47 additions & 15 deletions packages/sdk/cloudflare/src/createFeatureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,75 @@ import {
LDFeatureStoreItem,
LDFeatureStoreKindData,
noop,
deserializePoll,
} from '@launchdarkly/js-server-sdk-common-edge';

const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LDLogger) => {
const key = `LD-Env-${sdkKey}`;
const store: LDFeatureStore = {
get(
kind: DataKind,
flagKey: string,
dataKey: string,
callback: (res: LDFeatureStoreItem | null) => void = noop
): void {
logger.debug(`Requesting ${flagKey} from ${key}`);
const { namespace } = kind;
const kindKey = namespace === 'features' ? 'flags' : namespace;
logger.debug(`Requesting ${dataKey} from ${key}.${kindKey}`);

kvNamespace
.get(key, { type: 'json' })
.get(key)
.then((i) => {
if (i === null) {
logger.error('Feature data not found in KV.');
if (!i) {
throw new Error(`${key}.${kindKey} is not found in KV.`);
}

const item = deserializePoll(i);
if (!item) {
throw new Error(`Error deserializing ${kindKey}`);
}

switch (namespace) {
case 'features':
callback(item.flags[dataKey]);
break;
case 'segments':
callback(item.segments[dataKey]);
break;
default:
throw new Error(`Unsupported DataKind: ${namespace}`);
}
const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace;
const item = i as LDFeatureStoreItem;
callback(item[kindKey][flagKey]);
})
.catch((err) => {
logger.error(err);
callback(null);
});
},
all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): void {
const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace;
logger.debug(`Requesting all ${kindKey} data from KV.`);
const { namespace } = kind;
const kindKey = namespace === 'features' ? 'flags' : namespace;
logger.debug(`Requesting all from ${key}.${kindKey}`);
kvNamespace
.get(key, { type: 'json' })
.get(key)
.then((i) => {
if (i === null) {
logger.error('Feature data not found in KV.');
if (!i) {
throw new Error(`${key}.${kindKey} is not found in KV.`);
}

const item = deserializePoll(i);
if (!item) {
throw new Error(`Error deserializing ${kindKey}`);
}

switch (namespace) {
case 'features':
callback(item.flags);
break;
case 'segments':
callback(item.segments);
break;
default:
throw new Error(`Unsupported DataKind: ${namespace}`);
}
const item = i as LDFeatureStoreItem;
callback(item[kindKey]);
})
.catch((err) => {
logger.error(err);
Expand Down
Loading