Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
00ecc9a
Start on Fastly SDK
ldhenry Dec 13, 2024
809e1cd
polyfill node:events in fastly sdk instead of example
ldhenry Dec 18, 2024
0914d82
Add tests and cleanup
ldhenry Jan 3, 2025
be7aa07
Fix test and revert change to sdk-server-edge
ldhenry Jan 3, 2025
40f05ab
Add CI
ldhenry Jan 3, 2025
77485da
Add crypto-js types
ldhenry Jan 3, 2025
aff265d
fix typedoc
ldhenry Jan 3, 2025
7d47d12
Add fastly to release-please
ldhenry Jan 3, 2025
cfddcdc
Prep for alpha release
ldhenry Jan 3, 2025
6079a15
Merge main
ldhenry Jan 3, 2025
f9b9561
Prep for 0.0.1 release
ldhenry Jan 3, 2025
7fd2d96
Update homepage url
ldhenry Jan 3, 2025
ce436a2
merge main
ldhenry Feb 10, 2025
3717d13
Add jsr.json and pin version in example
ldhenry Feb 10, 2025
e96311c
Change name in platformInfo
ldhenry Feb 10, 2025
9c7c966
lowercase in .sdk_metadata
ldhenry Feb 10, 2025
91819c9
Add eslint config
ldhenry Feb 10, 2025
584eef5
Add remaining eslint configs
ldhenry Feb 10, 2025
08cb672
Add more eslint helpers
ldhenry Feb 10, 2025
53891b1
Add @trivago/prettier-plugin-sort-imports
ldhenry Feb 10, 2025
1fb5431
Downgrade @typescript-eslint version
ldhenry Feb 10, 2025
9c6d3dd
Add ts-jest
ldhenry Feb 10, 2025
ed6764e
Add @types/jest
ldhenry Feb 10, 2025
4282427
Bump dependencies
ldhenry Feb 10, 2025
a422de4
Bump example version
ldhenry Feb 10, 2025
6271e7c
Clean up LDClient implementation and add eventsUri option
ldhenry Feb 10, 2025
dff0e69
Bump @fastly/cli
ldhenry Feb 10, 2025
7c1bc03
Use release-please to set version
ldhenry Feb 10, 2025
2eef68d
Fix test
ldhenry Feb 10, 2025
b41b07b
Remove the need for ts-ignore
ldhenry Feb 10, 2025
dda0ce5
Add newline to tsconfig.json
ldhenry Feb 10, 2025
389c52e
Swap photos with PD versions
ldhenry Feb 19, 2025
a73b761
Reformat photo credits
ldhenry Feb 19, 2025
0111854
merge main
ldhenry Feb 19, 2025
3a67b2b
Add link to free Fastly account
ldhenry Feb 19, 2025
36d8136
fix ci build
ldhenry Feb 26, 2025
c567c1a
Bump dependencies to fix build
ldhenry Feb 26, 2025
7cd6bff
Prep for beta release
ldhenry Feb 26, 2025
c90f23a
Also update package.json
ldhenry Feb 26, 2025
fa71eb6
rest version and update example
ldhenry Feb 26, 2025
8cb7fd0
PR feedback
ldhenry Mar 3, 2025
67bde4a
Merge branch 'main' into hbarrow/REL-4756/create-fastly-sdk-in-js-core
ldhenry Mar 3, 2025
58c7cd2
Move mocks to __test__ dir
ldhenry Mar 4, 2025
7047f2f
Improve validation
ldhenry Mar 5, 2025
3eea8c2
Fix lint errors
ldhenry Mar 5, 2025
ccd9d42
Don't pass in KV store
ldhenry Mar 6, 2025
01f9784
Move ldClient to the top
ldhenry Mar 6, 2025
98bb0e6
Revert moving store init
ldhenry Mar 6, 2025
bf72203
Add lazy loader
ldhenry Mar 6, 2025
fd91a2a
Update README
ldhenry Mar 6, 2025
ecaad7b
Revert changes to example
ldhenry Mar 7, 2025
7beddc1
PR feedback
ldhenry Mar 7, 2025
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
7 changes: 0 additions & 7 deletions packages/sdk/fastly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@

The LaunchDarkly SDK for Fastly is designed for use in [Fastly Compute Platform](https://www.fastly.com/documentation/guides/compute/). It follows the server-side LaunchDarkly model for multi-user contexts. It is not intended for use in desktop and embedded systems applications.

# ⛔️⛔️⛔️⛔️

> [!CAUTION]
> This library is an alpha version and should not be considered ready for production use while this message is visible.

# ☝️☝️☝️☝️☝️☝️

## Install

```shell
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common';

import { EdgeFeatureStore } from '../../src/api/EdgeFeatureStore';
import mockEdgeProvider from '../../src/utils/mockEdgeProvider';
import mockEdgeProvider from '../utils/mockEdgeProvider';
import * as testData from './testData.json';

describe('EdgeFeatureStore', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/fastly/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('init', () => {
beforeAll(async () => {
mockKVStore = new KVStore('test-kv-store') as jest.Mocked<KVStore>;
const testDataString = JSON.stringify(testData);

mockKVStore.get.mockResolvedValue({
text: jest.fn().mockResolvedValue(testDataString),
json: jest.fn().mockResolvedValue(testData),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EdgeProvider } from '../api';
import { EdgeProvider } from '../../src/api';

const mockEdgeProvider: EdgeProvider = {
get: jest.fn(),
Expand Down
8 changes: 4 additions & 4 deletions packages/sdk/fastly/__tests__/utils/validateOptions.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { BasicLogger } from '@launchdarkly/js-server-sdk-common';

import mockFeatureStore from '../../src/utils/mockFeatureStore';
import validateOptions from '../../src/utils/validateOptions';
import mockFeatureStore from './mockFeatureStore';

describe('validateOptions', () => {
test('throws without SDK key', () => {
expect(() => {
validateOptions('', {});
}).toThrowError(/You must configure the client with a client key/);
}).toThrow(/You must configure the client with a client-side id/);
});

test('throws without featureStore', () => {
expect(() => {
validateOptions('test-sdk-key', {});
}).toThrowError(/You must configure the client with a feature store/);
}).toThrow(/You must configure the client with a feature store/);
});

test('throws without logger', () => {
expect(() => {
validateOptions('test-sdk-key', { featureStore: mockFeatureStore });
}).toThrowError(/You must configure the client with a logger/);
}).toThrow(/You must configure the client with a logger/);
});

test('success valid options', () => {
Expand Down
28 changes: 27 additions & 1 deletion packages/sdk/fastly/example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { init } from '@launchdarkly/fastly-server-sdk';
import type { LDMultiKindContext } from '@launchdarkly/js-server-sdk-common';

// Set your LaunchDarkly client ID here
const LAUNCHDARKLY_CLIENT_ID = '<your-client-id>';
const LAUNCHDARKLY_CLIENT_ID = '675aea6b1b327709c85da941';
// Set the KV store name used to store the LaunchDarkly data here
const KV_STORE_NAME = 'launchdarkly';
// Set the Fastly Backend name used to send LaunchDarkly events here
Expand Down Expand Up @@ -71,6 +71,32 @@ async function handleRequest(event: FetchEvent) {

const url = new URL(req.url);

if (url.pathname === '/loop') {
const flagValues: boolean[] = [];

// Run evaluations in series instead of parallel
const runEvaluationsInSeries = async (
count: number,
results: boolean[],
): Promise<boolean[]> => {
if (count <= 0) {
return results;
}

const result = await ldClient.boolVariation('example-flag', flagContext, false);
results.push(result);

return runEvaluationsInSeries(count - 1, results);
};

await runEvaluationsInSeries(35, flagValues);

return new Response(JSON.stringify(flagValues, undefined, 2), {
status: 200,
headers: new Headers({ 'Content-Type': 'application/json' }),
});
}

if (url.pathname === '/') {
const flagKey = 'example-flag';
const variationDetail = await ldClient.boolVariationDetail(flagKey, flagContext, false);
Expand Down
19 changes: 16 additions & 3 deletions packages/sdk/fastly/src/api/EdgeFeatureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface EdgeProvider {

export class EdgeFeatureStore implements LDFeatureStore {
private readonly _rootKey: string;
private _kvData: string | null | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe only undefined is necessary for this situation. Is there another API that has both null and undefined that is causing you to use both here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Fastly KV fetch method returns null when the KV item does not exist and we have a null check in the initialized method. However, I don't think undefined is needed so I removed that.


constructor(
private readonly _edgeProvider: EdgeProvider,
Expand All @@ -24,6 +25,18 @@ export class EdgeFeatureStore implements LDFeatureStore {
this._rootKey = `LD-Env-${sdkKey}`;
}

/**
* This function is used to lazy load the KV data from the edge provider. This is necessary because Fastly Compute
* has a limit of 32 backend requests (including requests to fetch the KV data).
* https://docs.fastly.com/products/compute-resource-limits
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you want to describe in the readme or public APIs that data won't receive updates during the runtime of the fastly process/execution/request handler lifecycle?

(I think I have that right)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback. I just added a Usage Notes section to the README that contains this.

private async _getKVData(): Promise<string | null | undefined> {
if (!this._kvData) {
this._kvData = await this._edgeProvider.get(this._rootKey);
}
return this._kvData;
}

async get(
kind: DataKind,
dataKey: string,
Expand All @@ -34,7 +47,7 @@ export class EdgeFeatureStore implements LDFeatureStore {
this._logger.debug(`Requesting ${dataKey} from ${this._rootKey}.${kindKey}`);

try {
const i = await this._edgeProvider.get(this._rootKey);
const i = await this._getKVData();

if (!i) {
throw new Error(`${this._rootKey}.${kindKey} is not found in KV.`);
Expand Down Expand Up @@ -66,7 +79,7 @@ export class EdgeFeatureStore implements LDFeatureStore {
const kindKey = namespace === 'features' ? 'flags' : namespace;
this._logger.debug(`Requesting all from ${this._rootKey}.${kindKey}`);
try {
const i = await this._edgeProvider.get(this._rootKey);
const i = await this._getKVData();
if (!i) {
throw new Error(`${this._rootKey}.${kindKey} is not found in KV.`);
}
Expand All @@ -93,7 +106,7 @@ export class EdgeFeatureStore implements LDFeatureStore {
}

async initialized(callback: (isInitialized: boolean) => void = noop): Promise<void> {
const config = await this._edgeProvider.get(this._rootKey);
const config = await this._getKVData();
const result = config !== null;
this._logger.debug(`Is ${this._rootKey} initialized? ${result}`);
callback(result);
Expand Down
18 changes: 12 additions & 6 deletions packages/sdk/fastly/src/utils/validateOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LDOptions as LDOptionsCommon } from '@launchdarkly/js-server-sdk-common';
import { LDOptions as LDOptionsCommon, TypeValidators } from '@launchdarkly/js-server-sdk-common';

/**
* The Launchdarkly Fastly Compute SDK configuration options.
Expand All @@ -16,17 +16,23 @@ export type FastlySDKOptions = Pick<LDOptionsCommon, 'logger' | 'sendEvents' | '
*/
export type LDOptionsInternal = FastlySDKOptions & Pick<LDOptionsCommon, 'featureStore'>;

const validateOptions = (sdkKey: string, options: LDOptionsInternal) => {
const validators = {
clientSideId: TypeValidators.String,
featureStore: TypeValidators.ObjectOrFactory,
logger: TypeValidators.Object,
};

const validateOptions = (clientSideId: string, options: LDOptionsInternal) => {
const { eventsBackendName, featureStore, logger, sendEvents, ...rest } = options;
if (!sdkKey) {
throw new Error('You must configure the client with a client key');
if (!clientSideId || !validators.clientSideId.is(clientSideId)) {
throw new Error('You must configure the client with a client-side id');
}

if (!featureStore || typeof featureStore !== 'object' || !featureStore.get) {
if (!featureStore || !validators.featureStore.is(featureStore)) {
throw new Error('You must configure the client with a feature store');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Here is the logic used by the type validators for the feature store. https://github.com/launchdarkly/js-core/blob/main/packages/shared/common/src/validators.ts#L22

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I started down this road and it seems promising. I have to sign off soon but I should have your suggestion in first thing tomorrow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just updated it to use type validators. Thanks for pointing this out.


if (!logger) {
if (!logger || !validators.logger.is(logger)) {
throw new Error('You must configure the client with a logger');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the logger is optional on most SDKs.

   * If you do not set this property, the SDK uses {@link basicLogger} with a
   * minimum level of `info`.
   */
  logger?: LDLogger;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are always setting a logger here if not specified so the additional validation in validateOptions is maybe a little redundant.


Expand Down