Skip to content

Commit

Permalink
convert SvelteKit to use core
Browse files Browse the repository at this point in the history
  • Loading branch information
dferber90 committed Feb 23, 2025
1 parent 2de3b33 commit d4e9c74
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 160 deletions.
3 changes: 2 additions & 1 deletion packages/flags/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
},
"dependencies": {
"@edge-runtime/cookies": "^5.0.2",
"jose": "^5.2.1"
"jose": "^5.2.1",
"json-stringify-deterministic": "1.0.12"
},
"devDependencies": {
"@arethetypeswrong/cli": "0.17.3",
Expand Down
6 changes: 5 additions & 1 deletion packages/flags/src/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* by the implementations for different frameworks.
*/

import stringify from 'json-stringify-deterministic';
import { isInternalNextError } from '../next/is-internal-next-error';
import { getCachedValuePromise, setCachedValuePromise } from './request-cache';
import { setSpanAttribute } from './tracing';
Expand Down Expand Up @@ -96,6 +97,7 @@ export async function core<ValueType, EntitiesType>({
readonlyHeaders,
readonlyCookies,
shouldReportValue,
secret,
}: {
flagKey: string;
getPrecomputed?: () => Promise<ValueType>;
Expand All @@ -106,6 +108,7 @@ export async function core<ValueType, EntitiesType>({
readonlyHeaders: ReadonlyHeaders;
readonlyCookies: ReadonlyRequestCookies;
shouldReportValue: boolean;
secret?: string;
}) {
if (typeof getPrecomputed === 'function') {
const precomputed = await getPrecomputed();
Expand All @@ -117,6 +120,7 @@ export async function core<ValueType, EntitiesType>({

const overrides = await getOverrides(
readonlyCookies.get('vercel-flag-overrides')?.value,
secret,
);
const override = overrides ? overrides?.[flagKey] : null;
if (overrides) {
Expand All @@ -132,7 +136,7 @@ export async function core<ValueType, EntitiesType>({
readonlyCookies,
);

const entitiesKey = JSON.stringify(entities) ?? '';
const entitiesKey = stringify(entities) ?? '';

const cachedValue = getCachedValuePromise(
requestCacheKey,
Expand Down
11 changes: 7 additions & 4 deletions packages/flags/src/lib/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { type FlagOverridesType, decrypt } from '..';
import { memoizeOne } from './async-memoize-one';

const memoizedDecrypt = memoizeOne(
(text: string) => decrypt<FlagOverridesType>(text),
(a, b) => a[0] === b[0], // only the first argument gets compared
(text: string, secret?: string) => decrypt<FlagOverridesType>(text, secret),
(a, b) => a[0] === b[0] && a[1] === b[1],
{ cachePromiseRejection: true },
);

export async function getOverrides(cookie: string | undefined) {
export async function getOverrides(
cookie: string | undefined,
secret?: string,
) {
if (typeof cookie === 'string' && cookie !== '') {
const cookieOverrides = await memoizedDecrypt(cookie);
const cookieOverrides = await memoizedDecrypt(cookie, secret);
return cookieOverrides ?? null;
}

Expand Down
File renamed without changes.
21 changes: 21 additions & 0 deletions packages/flags/src/lib/resolve-object-promises.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type PromisesMap<T> = {
[K in keyof T]: Promise<T[K]>;
};

export async function resolveObjectPromises<T>(
obj: PromisesMap<T>,
): Promise<T> {
// Convert the object into an array of [key, promise] pairs
const entries = Object.entries(obj) as [keyof T, Promise<any>][];

// Use Promise.all to wait for all the promises to resolve
const resolvedEntries = await Promise.all(
entries.map(async ([key, promise]) => {
const value = await promise;
return [key, value] as [keyof T, T[keyof T]];
}),
);

// Convert the array of resolved [key, value] pairs back into an object
return Object.fromEntries(resolvedEntries) as T;
}
5 changes: 3 additions & 2 deletions packages/flags/src/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
sealCookies,
sealHeaders,
transformToHeaders,
} from './request-mapping';
} from '../lib/request-mapping';

export type { Flag } from './types';

Expand Down Expand Up @@ -74,7 +74,7 @@ function getRun<ValueType, EntitiesType>(
requestCacheKey = headersStore;
}

return core({
return core<ValueType, EntitiesType>({
readonlyHeaders,
readonlyCookies,
flagKey: definition.key,
Expand All @@ -83,6 +83,7 @@ function getRun<ValueType, EntitiesType>(
requestCacheKey,
defaultValue: definition.defaultValue,
shouldReportValue: definition.config?.reportValue !== false,
secret: process.env.FLAGS_SECRET,
});
};
}
Expand Down
148 changes: 35 additions & 113 deletions packages/flags/src/sveltekit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,18 @@ import type { Handle, RequestEvent } from '@sveltejs/kit';
import { AsyncLocalStorage } from 'node:async_hooks';
import {
type ApiData,
decrypt,
encrypt,
reportValue,
safeJsonStringify,
verifyAccess,
type JsonValue,
type FlagDefinitionsType,
} from '..';
import { Decide, FlagDeclaration, GenerousOption } from '../types';
import {
type ReadonlyHeaders,
HeadersAdapter,
} from '../spec-extension/adapters/headers';
import {
type ReadonlyRequestCookies,
RequestCookiesAdapter,
} from '../spec-extension/adapters/request-cookies';
import { FlagDeclaration, GenerousOption } from '../types';
import { normalizeOptions } from '../lib/normalize-options';
import { RequestCookies } from '@edge-runtime/cookies';

function hasOwnProperty<X extends {}, Y extends PropertyKey>(
obj: X,
prop: Y,
): obj is X & Record<Y, unknown> {
return obj.hasOwnProperty(prop);
}

const headersMap = new WeakMap<Headers, ReadonlyHeaders>();
const cookiesMap = new WeakMap<Headers, ReadonlyRequestCookies>();

function sealHeaders(headers: Headers): ReadonlyHeaders {
const cached = headersMap.get(headers);
if (cached !== undefined) return cached;

const sealed = HeadersAdapter.seal(headers);
headersMap.set(headers, sealed);
return sealed;
}

function sealCookies(headers: Headers): ReadonlyRequestCookies {
const cached = cookiesMap.get(headers);
if (cached !== undefined) return cached;

const sealed = RequestCookiesAdapter.seal(new RequestCookies(headers));
cookiesMap.set(headers, sealed);
return sealed;
}
import { core, getDecide, getIdentify } from '../lib/core';
import { getOrigin } from '../lib/origin';
import { sealCookies, sealHeaders } from '../lib/request-mapping';
import { resolveObjectPromises } from '../lib/resolve-object-promises';

type Flag<ReturnValue> = (() => ReturnValue | Promise<ReturnValue>) & {
key: string;
Expand All @@ -57,87 +22,43 @@ type Flag<ReturnValue> = (() => ReturnValue | Promise<ReturnValue>) & {
options?: GenerousOption<ReturnValue>[];
};

type PromisesMap<T> = {
[K in keyof T]: Promise<T[K]>;
};

async function resolveObjectPromises<T>(obj: PromisesMap<T>): Promise<T> {
// Convert the object into an array of [key, promise] pairs
const entries = Object.entries(obj) as [keyof T, Promise<any>][];

// Use Promise.all to wait for all the promises to resolve
const resolvedEntries = await Promise.all(
entries.map(async ([key, promise]) => {
const value = await promise;
return [key, value] as [keyof T, T[keyof T]];
}),
);

// Convert the array of resolved [key, value] pairs back into an object
return Object.fromEntries(resolvedEntries) as T;
}

function getDecide<ValueType, EntitiesType>(
definition: FlagDeclaration<ValueType, EntitiesType>,
): Decide<ValueType, EntitiesType> {
return function decide(params) {
if (typeof definition.decide === 'function') {
return definition.decide(params);
}
if (typeof definition.adapter?.decide === 'function') {
return definition.adapter.decide({ key: definition.key, ...params });
}
throw new Error(`flags: No decide function provided for ${definition.key}`);
};
}

/**
* Declares a feature flag
*/
export function flag<T>(definition: FlagDeclaration<T, unknown>): Flag<T> {
const decide = getDecide<T, unknown>(definition);

const flagImpl = async function flagImpl(): Promise<T> {
export function flag<
ValueType extends JsonValue = boolean | string | number,
EntitiesType = any,
>(definition: FlagDeclaration<ValueType, EntitiesType>): Flag<ValueType> {
const decide = getDecide<ValueType, EntitiesType>(definition);
const identify = getIdentify<ValueType, EntitiesType>(definition);
const origin = getOrigin(definition);

const flagImpl = async function flagImpl(): Promise<ValueType> {
const store = flagStorage.getStore();

if (!store) {
throw new Error('flags: context not found');
}

if (hasOwnProperty(store.usedFlags, definition.key)) {
const valuePromise = store.usedFlags[definition.key];
if (typeof valuePromise !== 'undefined') {
return valuePromise as Promise<T>;
}
}

const overridesCookie = store.event.cookies.get('vercel-flag-overrides');
const overrides = overridesCookie
? await decrypt<Record<string, T>>(overridesCookie, store.secret)
: undefined;

if (overrides && hasOwnProperty(overrides, definition.key)) {
const value = overrides[definition.key];
if (typeof value !== 'undefined') {
reportValue(definition.key, value);
store.usedFlags[definition.key] = Promise.resolve(value as JsonValue);
return value;
}
}

const valuePromise = decide(
{
headers: sealHeaders(store.event.request.headers),
cookies: sealCookies(store.event.request.headers),
},
// @ts-expect-error not part of the type, but we supply it for convenience
{ event: store.event },
);
store.usedFlags[definition.key] = valuePromise as Promise<JsonValue>;

const value = await valuePromise;
reportValue(definition.key, value);
return value;
const readonlyHeaders = sealHeaders(store.event.request.headers);
const readonlyCookies = sealCookies(store.event.request.headers);

const decisionPromise = core<ValueType, EntitiesType>({
readonlyHeaders,
readonlyCookies,
flagKey: definition.key,
identify,
decide,
requestCacheKey: store.event.request,
defaultValue: definition.defaultValue,
shouldReportValue: definition.config?.reportValue !== false,
secret: store.secret,
});

// report for handler
store.usedFlags[definition.key] = decisionPromise as Promise<JsonValue>;

return decisionPromise;
};

flagImpl.key = definition.key;
Expand All @@ -146,7 +67,8 @@ export function flag<T>(definition: FlagDeclaration<T, unknown>): Flag<T> {
flagImpl.description = definition.description;
flagImpl.options = definition.options;
flagImpl.decide = decide;
// flagImpl.identify = definition.identify;
flagImpl.origin = origin;
flagImpl.identify = identify;

return flagImpl;
}
Expand Down
Loading

0 comments on commit d4e9c74

Please sign in to comment.