Skip to content

Commit 309fa13

Browse files
committed
fix(node-sdk): lazy load fallback provider clients
1 parent 3fbdfa2 commit 309fa13

2 files changed

Lines changed: 87 additions & 21 deletions

File tree

packages/node-sdk/src/flagsFallbackProvider.ts

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import { promises as fs } from "fs";
22
import path from "path";
3-
import {
4-
GetObjectCommand,
5-
NoSuchKey,
6-
PutObjectCommand,
7-
S3Client,
8-
} from "@aws-sdk/client-s3";
9-
import { Storage } from "@google-cloud/storage";
10-
import { createClient as createRedisClient } from "@redis/client";
113

124
import type {
135
FlagAPIResponse,
@@ -35,7 +27,9 @@ export type S3FallbackProviderOptions = {
3527
/**
3628
* Optional S3 client. A default client is created when omitted.
3729
*/
38-
client?: Pick<S3Client, "send">;
30+
client?: {
31+
send(command: unknown): Promise<any>;
32+
};
3933

4034
/**
4135
* Prefix for generated per-environment keys.
@@ -54,7 +48,18 @@ export type GCSFallbackProviderOptions = {
5448
/**
5549
* Optional GCS client. A default client is created when omitted.
5650
*/
57-
client?: Pick<Storage, "bucket">;
51+
client?: {
52+
bucket(name: string): {
53+
file(path: string): {
54+
exists(): Promise<[boolean]>;
55+
download(): Promise<[Uint8Array]>;
56+
save(
57+
body: string,
58+
options: { contentType: string },
59+
): Promise<unknown>;
60+
};
61+
};
62+
};
5863

5964
/**
6065
* Prefix for generated per-environment keys.
@@ -160,6 +165,16 @@ function parseSnapshot(raw: string) {
160165
return isFlagsFallbackSnapshot(parsed) ? parsed : undefined;
161166
}
162167

168+
async function createDefaultS3Client() {
169+
const { S3Client } = await import("@aws-sdk/client-s3");
170+
return new S3Client({});
171+
}
172+
173+
async function createDefaultGCSClient() {
174+
const { Storage } = await import("@google-cloud/storage");
175+
return new Storage();
176+
}
177+
163178
export function createFileFallbackProvider({
164179
directory,
165180
}: FileFallbackProviderOptions = {}): FlagsFallbackProvider {
@@ -189,13 +204,27 @@ export function createFileFallbackProvider({
189204

190205
export function createS3FallbackProvider({
191206
bucket,
192-
client = new S3Client({}),
207+
client,
193208
keyPrefix,
194209
}: S3FallbackProviderOptions): FlagsFallbackProvider {
210+
let defaultClient:
211+
| {
212+
send(command: unknown): Promise<any>;
213+
}
214+
| undefined;
215+
216+
const getClient = async () => {
217+
defaultClient ??= client ?? (await createDefaultS3Client());
218+
return defaultClient;
219+
};
220+
195221
return {
196222
async load(context) {
223+
const s3 = await getClient();
224+
const { GetObjectCommand } = await import("@aws-sdk/client-s3");
225+
197226
try {
198-
const response = await client.send(
227+
const response = await s3.send(
199228
new GetObjectCommand({
200229
Bucket: bucket,
201230
Key: snapshotObjectKey(context, keyPrefix),
@@ -208,7 +237,6 @@ export function createS3FallbackProvider({
208237
return parseSnapshot(body);
209238
} catch (error: any) {
210239
if (
211-
error instanceof NoSuchKey ||
212240
error?.name === "NoSuchKey" ||
213241
error?.$metadata?.httpStatusCode === 404
214242
) {
@@ -219,7 +247,10 @@ export function createS3FallbackProvider({
219247
},
220248

221249
async save(context, snapshot) {
222-
await client.send(
250+
const s3 = await getClient();
251+
const { PutObjectCommand } = await import("@aws-sdk/client-s3");
252+
253+
await s3.send(
223254
new PutObjectCommand({
224255
Bucket: bucket,
225256
Key: snapshotObjectKey(context, keyPrefix),
@@ -233,12 +264,20 @@ export function createS3FallbackProvider({
233264

234265
export function createGCSFallbackProvider({
235266
bucket,
236-
client = new Storage(),
267+
client,
237268
keyPrefix,
238269
}: GCSFallbackProviderOptions): FlagsFallbackProvider {
270+
let defaultClient: GCSFallbackProviderOptions["client"] | undefined;
271+
272+
const getClient = async () => {
273+
defaultClient ??= client ?? (await createDefaultGCSClient());
274+
return defaultClient;
275+
};
276+
239277
return {
240278
async load(context) {
241-
const file = client
279+
const storage = await getClient();
280+
const file = storage
242281
.bucket(bucket)
243282
.file(snapshotObjectKey(context, keyPrefix));
244283
const [exists] = await file.exists();
@@ -247,11 +286,12 @@ export function createGCSFallbackProvider({
247286
}
248287

249288
const [contents] = await file.download();
250-
return parseSnapshot(contents.toString("utf-8"));
289+
return parseSnapshot(Buffer.from(contents).toString("utf-8"));
251290
},
252291

253292
async save(context, snapshot) {
254-
await client
293+
const storage = await getClient();
294+
await storage
255295
.bucket(bucket)
256296
.file(snapshotObjectKey(context, keyPrefix))
257297
.save(JSON.stringify(snapshot), {
@@ -280,9 +320,16 @@ export function createRedisFallbackProvider({
280320
return client;
281321
}
282322

283-
defaultClient ??= createRedisClient(
284-
process.env.REDIS_URL ? { url: process.env.REDIS_URL } : undefined,
285-
);
323+
if (!process.env.REDIS_URL) {
324+
throw new Error(
325+
"fallbackProviders.redis() requires REDIS_URL to be set when no client is provided",
326+
);
327+
}
328+
329+
if (!defaultClient) {
330+
const { createClient } = await import("@redis/client");
331+
defaultClient = createClient({ url: process.env.REDIS_URL });
332+
}
286333

287334
if (!defaultClient.isOpen) {
288335
connectPromise ??= defaultClient.connect();

packages/node-sdk/test/flagsFallbackProvider.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,23 @@ describe("flagsFallbackProvider", () => {
233233
JSON.stringify(snapshot),
234234
);
235235
});
236+
237+
it("requires REDIS_URL when no Redis client is provided", async () => {
238+
const previousRedisUrl = process.env.REDIS_URL;
239+
delete process.env.REDIS_URL;
240+
241+
try {
242+
const provider = fallbackProviders.redis();
243+
244+
await expect(provider.load(context)).rejects.toThrow(
245+
"fallbackProviders.redis() requires REDIS_URL to be set when no client is provided",
246+
);
247+
} finally {
248+
if (previousRedisUrl === undefined) {
249+
delete process.env.REDIS_URL;
250+
} else {
251+
process.env.REDIS_URL = previousRedisUrl;
252+
}
253+
}
254+
});
236255
});

0 commit comments

Comments
 (0)