Skip to content

Commit

Permalink
fix launchdarkly example (#893)
Browse files Browse the repository at this point in the history
When using LaunchDarkly developers need to first initialize the
LaunchDarkly client like so

```ts
import { createClient } from '@vercel/edge-config'
import { LDClient, init } from '@launchdarkly/vercel-server-sdk'

// create the edge config and launchdarkly clients
const edgeConfigClient = createClient(process.env.EDGE_CONFIG)
const ldClient = init(process.env.NEXT_PUBLIC_LD_CLIENT_SIDE_ID, edgeConfigClient)

// wait for it to init of the launchdarkly client
await ldClient.waitForInitialization()

// use the launchdarkly client
const flagValue = await ldClient.variation('my-flag', {}, true)
```

Intuitively a developer would do this

```ts
export const runtime = 'edge'

const edgeClient = createClient(process.env.EDGE_CONFIG)
const ldClient = init(process.env.NEXT_PUBLIC_LD_CLIENT_SIDE_ID!, edgeClient)

export default async function Home() {
  await ldClient.waitForInitialization()
  const flagValue = await ldClient.variation('my-flag', {}, true)
```

But this has a huge issue: It is not allowed to share promises across
requests in Edge Runtime - more specifically in Cloudflare Workers which
Edge Functions build on. When this page gets requested twice, then the
first call to `ldClient.waitForInitialization()` creates a promise and
the second call will await the promise created by the first request.
This then leads to Cloudflare Workers throwing this error. But
`waitForInitialization` catches that error and retries indefinitely
until the function itself times out.

To fix this, we could create a fresh client from within the home
component like so

```ts
const edgeConfigClient = createClient(process.env.EDGE_CONFIG)

async function getLdClient(): Promise<LDClient> {
  const ldClient = init(
    process.env.NEXT_PUBLIC_LD_CLIENT_SIDE_ID,
    edgeConfigClient
  )

  await ldClient.waitForInitialization()

  return ldClient
}

export default async function Home() {
  // get client from within the component so we get a fresh instance for every
  // request, otherwise LaunchDarkly might share promises across requests, which
  // can leads to timeouts in Edge Runtime
  const ldClient = await getLdClient()
```

But this has the issue that every call to `getLdClient` would create a
fresh instance, even if the calls to `getLdClient` happen for the same
request.

To solve this, we can wrap `getLdClient` in `cache`, which caches the
client for the duration of the server request and resets for each server
request.
  • Loading branch information
dferber90 committed Apr 3, 2024
1 parent a825fe9 commit 952de64
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 32 deletions.
40 changes: 36 additions & 4 deletions edge-middleware/feature-flag-launchdarkly/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Text, Page, Link } from '@vercel/examples-ui'
import { init } from '@launchdarkly/vercel-server-sdk'
import { type LDClient, init } from '@launchdarkly/vercel-server-sdk'
import { createClient } from '@vercel/edge-config'
import { cache } from 'react'

export const metadata = {
title: 'Vercel x LaunchDarkly example',
Expand All @@ -9,12 +10,43 @@ export const metadata = {
}
export const runtime = 'edge'

const edgeClient = createClient(process.env.EDGE_CONFIG)
const ldClient = init(process.env.NEXT_PUBLIC_LD_CLIENT_SIDE_ID!, edgeClient)
const edgeConfigClient = createClient(process.env.EDGE_CONFIG)

// In Edge Runtime it's not possible to share promises across requests.
//
// waitForInitialization attempts to use a pending promise if one exists, so
// we need to create a new client for every request to prevent reading a promise
// created by an earlier request.
//
// However, we still want to allow reusing the LaunchDarkly client for the
// duration of a specific request. This allows multiple components
// to call getLdClient() and to get the same instance. We use cache() to
// keep the LaunchDarkly client around for the duration of a request.
//
// cache resets for each server request, so a subsequent request will receive
// a new instance of the LaunchDarkly client.
//
// Notes
// - This setup is only necessary for Edge Functions, not for Serverless Functions
// - When using the LaunchDarkly client in Edge Middleware make sure to use
// a fresh instance for every request, as it has the same promise sharing
// problem as Edge Functions otherwise.
// - "cache" does not work in Edge Middleware, so you'd need to create a fresh
// instance of the LaunchDarkly client for every request.
const getLdClient = cache(async (): Promise<LDClient> => {
const ldClient = init(
process.env.NEXT_PUBLIC_LD_CLIENT_SIDE_ID!,
edgeConfigClient
)
await ldClient.waitForInitialization()
return ldClient
})

export default async function Home() {
const before = Date.now()
await ldClient.waitForInitialization()

const ldClient = await getLdClient()

const ldContext = {
kind: 'org',
key: 'my-org-key',
Expand Down
4 changes: 2 additions & 2 deletions edge-middleware/feature-flag-launchdarkly/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"lint": "next lint"
},
"dependencies": {
"@launchdarkly/vercel-server-sdk": "^1.1.1",
"@vercel/edge-config": "^0.2.1",
"@launchdarkly/vercel-server-sdk": "^1.3.3",
"@vercel/edge-config": "^1.1.0",
"@vercel/examples-ui": "^2.0.3",
"next": "^13.4.19",
"react": "^18.2.0",
Expand Down
70 changes: 44 additions & 26 deletions edge-middleware/feature-flag-launchdarkly/pnpm-lock.yaml

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

0 comments on commit 952de64

Please sign in to comment.