Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 12 additions & 1 deletion packages/sdk/fastly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,18 @@ yarn add @launchdarkly/fastly-server-sdk

- The SDK must be initialized and used when processing requests, not during build-time initialization.
- The SDK caches all KV data during initialization to reduce the number of backend requests needed to fetch KV data. This means changes to feature flags or segments will not be picked up during the lifecycle of a single request instance.
- Events should flushed using the [`waitUntil()` method](https://js-compute-reference-docs.edgecompute.app/docs/globals/FetchEvent/prototype/waitUntil).

### Events and the client lifecycle

The SDK is designed to be created per request: call `init()` inside your request handler, and flush events at the end of each request using the [`waitUntil()` method](https://js-compute-reference-docs.edgecompute.app/docs/globals/FetchEvent/prototype/waitUntil):

```js
event.waitUntil(ldClient.flush());
```

The SDK does not run a periodic background flush. On [Fastly Compute](https://www.fastly.com/documentation/guides/compute/developer-guides/sandbox-lifecycle/), outbound requests must be made while a request is being handled, and by default each request runs in a fresh sandbox that is torn down afterward, so a background timer cannot reliably deliver events. Flushing explicitly with `waitUntil()` on every request that evaluates flags is the supported pattern -- it keeps the sandbox alive just long enough to send the events after the response is returned.

If you enable Fastly's opt-in [reusable sandboxes](https://www.fastly.com/documentation/guides/compute/developer-guides/sandbox-lifecycle/), a single sandbox may serve multiple requests, so avoid holding a single client at module scope and relying on it to flush on its own: buffered events would accumulate and, once the event queue capacity is reached, be dropped. Creating a client per request and flushing via `waitUntil()` avoids this regardless of sandbox mode.

## Quickstart

Expand Down
13 changes: 13 additions & 0 deletions packages/sdk/fastly/__tests__/api/LDClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ describe('Edge LDClient', () => {
},
});
});
it('constructs the event processor without background flush timers', async () => {
const client = new LDClient('client-side-id', createBasicPlatform().info, {
sendEvents: true,
eventsBackendName: 'launchdarkly',
});
await client.waitForInitialization({ timeout: 10 });

// The 6th positional argument to EventProcessor is `start`. Edge clients run per
// request and flush explicitly, so they must not start the background flush timers.
expect(mockEventProcessor).toHaveBeenCalled();
expect(mockEventProcessor.mock.calls[0][5]).toBe(false);
});

it('uses custom eventsUri when specified', async () => {
const client = new LDClient('client-side-id', createBasicPlatform().info, {
sendEvents: true,
Expand Down
10 changes: 8 additions & 2 deletions packages/sdk/fastly/src/api/LDClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Info, internal, LDClientImpl } from '@launchdarkly/js-server-sdk-common';
import { Info, LDClientImpl, ServerInternalOptions } from '@launchdarkly/js-server-sdk-common';

import EdgePlatform from '../platform';
import { FastlySDKOptions } from '../utils/validateOptions';
Expand All @@ -18,10 +18,16 @@ export default class LDClient extends LDClientImpl {
platformInfo,
eventsBackendName || DEFAULT_EVENTS_BACKEND_NAME,
);
const internalOptions: internal.LDInternalOptions = {
const internalOptions: ServerInternalOptions = {
analyticsEventPath: `/events/bulk/${clientSideID}`,
diagnosticEventPath: `/events/diagnostic/${clientSideID}`,
includeAuthorizationHeader: false,
// Fastly Compute runs a fresh instance per request and flushes events explicitly
// via the request handler (event.waitUntil(client.flush())). Disable the event
// processor's background flush timers so a per-request client does not leave
// interval timers running that would keep the runtime alive and pin the client
// graph in memory.
disableBackgroundEventFlush: true,
};

const finalOptions = createOptions(ldOptions);
Expand Down
10 changes: 10 additions & 0 deletions packages/shared/sdk-server/src/LDClientImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ function constructFDv1(
dataSourceErrorHandler: (e: any) => void,
hooks: Hook[],
instanceId: string | undefined,
startEventProcessor: boolean,
): {
config: Configuration;
logger: LDLogger | undefined;
Expand Down Expand Up @@ -170,6 +171,7 @@ function constructFDv1(
baseHeaders,
new ContextDeduplicator(config),
diagnosticsManager,
startEventProcessor,
);
}

Expand Down Expand Up @@ -253,6 +255,7 @@ function constructFDv2(
initSuccess: () => void,
hooks: Hook[],
instanceId: string | undefined,
startEventProcessor: boolean,
): {
config: Configuration;
logger: LDLogger | undefined;
Expand Down Expand Up @@ -314,6 +317,7 @@ function constructFDv2(
baseHeaders,
new ContextDeduplicator(config),
diagnosticsManager,
startEventProcessor,
);
}

Expand Down Expand Up @@ -587,6 +591,10 @@ export default class LDClientImpl implements LDClient {
) {
const config = new Configuration(options, internalOptions);

// Edge SDKs run per request and flush events explicitly, so they opt out of the
// event processor's background flush timers to avoid leaving interval timers running.
const startEventProcessor = !internalOptions?.disableBackgroundEventFlush;

this.environmentMetadata = createPluginEnvironmentMetadata(_platform, _sdkKey, config);

const hooks: Hook[] = [];
Expand Down Expand Up @@ -620,6 +628,7 @@ export default class LDClientImpl implements LDClient {
(e) => this._dataSourceErrorHandler(e),
hooks,
internalOptions?.instanceId,
startEventProcessor,
));

this.bigSegmentStatusProviderInternal = this._bigSegmentsManager
Expand Down Expand Up @@ -657,6 +666,7 @@ export default class LDClientImpl implements LDClient {
() => this._initSuccess(),
hooks,
internalOptions?.instanceId,
startEventProcessor,
));
this._featureStore = transactionalStore;
this.bigSegmentStatusProviderInternal = this._bigSegmentsManager
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/sdk-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export * from './events';
export * from '@launchdarkly/js-sdk-common';
export * as internalServer from './internal';

export type { ServerInternalOptions } from './options/ServerInternalOptions';

export {
LDClientImpl,
BigSegmentStoreStatusProviderImpl,
Expand Down
13 changes: 13 additions & 0 deletions packages/shared/sdk-server/src/options/ServerInternalOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,17 @@ export interface ServerInternalOptions extends internal.LDInternalOptions {
* undefined and the header is omitted.
*/
instanceId?: string;

/**
* When true, the event processor is constructed without starting its periodic
* background work (the flush and context-deduplication interval timers, and the
* diagnostic timer when diagnostics are enabled). Events are still recorded and
* are delivered only via explicit flush() calls.
*
* This is intended for per-request edge SDKs that flush explicitly (for example
* via a runtime's waitUntil) and must not leave interval timers running. A live
* interval timer keeps the runtime event loop alive and roots the whole client
* graph in memory, which for a per-request client is a leak.
*/
disableBackgroundEventFlush?: boolean;
}
Loading