flags
Version:
Flags SDK by Vercel - The feature flags toolkit for Next.js and SvelteKit
1 lines • 12.3 kB
Source Map (JSON)
{"version":3,"sources":["../src/lib/normalize-options.ts","../src/lib/async-memoize-one.ts","../src/lib/serialization.ts"],"names":["matchIndex"],"mappings":"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","sourcesContent":["import type { FlagOption, GenerousOption } from '../types';\n\nexport function normalizeOptions<T>(\n flagOptions: GenerousOption<T>[] | undefined,\n): FlagOption<T>[] | undefined {\n if (!Array.isArray(flagOptions)) return flagOptions;\n\n return flagOptions.map((option) => {\n if (typeof option === 'boolean') return { value: option };\n if (typeof option === 'number') return { value: option };\n if (typeof option === 'string') return { value: option };\n if (option === null) return { value: option };\n\n return option;\n }) as FlagOption<T>[];\n}\n","// adapted from https://github.com/microlinkhq/async-memoize-one\n// and https://github.com/alexreardon/memoize-one\n\ntype MemoizeOneOptions = {\n cachePromiseRejection?: boolean;\n};\n\ntype MemoizedFn<TFunc extends (this: any, ...args: any[]) => any> = (\n this: ThisParameterType<TFunc>,\n ...args: Parameters<TFunc>\n) => ReturnType<TFunc>;\n\n/**\n * Memoizes an async function, but only keeps the latest result\n */\nexport function memoizeOne<TFunc extends (this: any, ...newArgs: any[]) => any>(\n fn: TFunc,\n isEqual: (a: Parameters<TFunc>, b: Parameters<TFunc>) => boolean,\n { cachePromiseRejection = false }: MemoizeOneOptions = {},\n): MemoizedFn<TFunc> {\n let calledOnce = false;\n let oldArgs: Parameters<TFunc>;\n let lastResult: any;\n\n function memoized(\n this: ThisParameterType<TFunc>,\n ...newArgs: Parameters<TFunc>\n ) {\n if (calledOnce && isEqual(newArgs, oldArgs)) return lastResult;\n\n lastResult = fn.apply(this, newArgs);\n\n if (!cachePromiseRejection && lastResult.catch) {\n lastResult.catch(() => (calledOnce = false));\n }\n\n calledOnce = true;\n oldArgs = newArgs;\n\n return lastResult;\n }\n\n return memoized;\n}\n","import type { JsonValue } from '..';\nimport { memoizeOne } from './async-memoize-one';\nimport type { FlagOption } from '../types';\nimport { CompactSign, base64url, compactVerify } from 'jose';\n\n// 252 max options length allows storing index 0 to 251,\n// so 252 is the first SPECIAL_INTEGER\nexport const MAX_OPTION_LENGTH = 252;\n\nenum SPECIAL_INTEGERS {\n /** Signals that the returned value is not listed in the flag's options */\n NULL = 252,\n BOOLEAN_FALSE = 253,\n BOOLEAN_TRUE = 254,\n UNLISTED_VALUE = 255,\n}\n\nconst memoizedVerify = memoizeOne(\n (code: string, secret: string) =>\n compactVerify(code, base64url.decode(secret), {\n algorithms: ['HS256'],\n }),\n (a, b) => a[0] === b[0] && a[1] === b[1], // only first two args matter\n { cachePromiseRejection: true },\n);\n\nconst memoizedSign = memoizeOne(\n (uint8Array: Uint8Array, secret) =>\n new CompactSign(uint8Array)\n .setProtectedHeader({ alg: 'HS256' })\n .sign(base64url.decode(secret)),\n (a, b) =>\n // matchedIndices array must be equal\n a[0].length === b[0].length &&\n a[0].every((v, i) => b[0][i] === v) &&\n // secrets must be equal\n a[1] === b[1],\n { cachePromiseRejection: true },\n);\n\nfunction splitUint8Array(\n array: Uint8Array,\n index: number,\n): [Uint8Array, Uint8Array] {\n const firstHalf = array.slice(0, index);\n const secondHalf = array.slice(index);\n return [firstHalf, secondHalf];\n}\n\n/**\n * Common subset of the flag type used in here\n */\ntype Flag = {\n key: string;\n options?: FlagOption<any>[];\n};\n\nexport async function deserialize(\n code: string,\n flags: readonly Flag[],\n secret: string,\n): Promise<Record<string, JsonValue>> {\n // TODO what happens when verification fails?\n const { payload } = await memoizedVerify(code, secret);\n\n const [matchedIndicesArray, valuesUint8Array] =\n payload.length === flags.length\n ? [payload]\n : splitUint8Array(payload, flags.length);\n\n const valuesArray = valuesUint8Array\n ? // re-add opening and closing brackets since we remove them when serializing\n JSON.parse(`[${new TextDecoder().decode(valuesUint8Array)}]`)\n : null;\n\n let spilled = 0;\n return matchedIndicesArray.reduce<Record<string, JsonValue>>(\n (acc, valueIndex, index) => {\n const flag = flags[index];\n\n if (!flag) {\n throw new Error(`flags: No flag at index ${index}`);\n }\n\n switch (valueIndex) {\n case SPECIAL_INTEGERS.BOOLEAN_FALSE:\n acc[flag.key] = false;\n break;\n case SPECIAL_INTEGERS.BOOLEAN_TRUE:\n acc[flag.key] = true;\n break;\n case SPECIAL_INTEGERS.UNLISTED_VALUE:\n acc[flag.key] = valuesArray[spilled++];\n break;\n case SPECIAL_INTEGERS.NULL:\n acc[flag.key] = null;\n break;\n default:\n acc[flag.key] = flag.options?.[valueIndex]?.value as JsonValue;\n }\n\n return acc;\n },\n {},\n );\n}\n\n/**\n * When serializing flags we find the matching option index for each evaluated value.\n *\n * This means we potentially need to iterate through all options of all flags.\n *\n * When the value we're trying to match is a literal (bool, string, number)\n * we look for it using referntial equality.\n *\n * When the value is an array or object we stringify the value and we stringify\n * the options of each flag and then we search for it by string comparison.\n *\n * This is faster than doing a deep equality check and also allows us not to\n * use any external library.\n *\n * We also cache the result of stringifying all options so in a Map so we only\n * ever need to stringify them once.\n */\nconst matchIndex = (function () {\n const stringifiedOptionsCache = new Map<FlagOption<any>[], string[]>();\n return function matchIndex(options: FlagOption<any>[], value: JsonValue) {\n const t = typeof value;\n\n // we're looking for a literal value, so we can check using referntial equality\n if (value === null || t === 'boolean' || t === 'string' || t === 'number') {\n return options.findIndex((v) => v.value === value);\n }\n\n // we're looking for an array or object, so we should check stringified\n const stringifiedValue = JSON.stringify(value);\n let stringifiedOptions = stringifiedOptionsCache.get(options);\n if (!stringifiedOptions) {\n stringifiedOptions = options.map((o) => JSON.stringify(o.value));\n stringifiedOptionsCache.set(options, stringifiedOptions);\n }\n\n return stringifiedOptions.findIndex(\n (stringifiedOption) => stringifiedOption === stringifiedValue,\n );\n };\n})();\n\nfunction joinUint8Arrays(array1: Uint8Array, array2: Uint8Array): Uint8Array {\n const combined = new Uint8Array(array1.length + array2.length);\n combined.set(array1);\n combined.set(array2, array1.length);\n return combined;\n}\nexport async function serialize(\n flagSet: Record<Flag['key'], JsonValue>,\n flags: readonly Flag[],\n secret: string,\n) {\n const unlistedValues: JsonValue[] = [];\n\n const matchedIndices = new Uint8Array(\n flags.map((flag) => {\n const options = Array.isArray(flag.options) ? flag.options : [];\n\n const value = flagSet[flag.key];\n if (\n !Object.prototype.hasOwnProperty.call(flagSet, flag.key) ||\n value === undefined\n ) {\n throw new Error(`flags: Missing value for flag \"${flag.key}\"`);\n }\n\n // avoid searching for common values\n // and ensure they can always be compressed, even if not listed in options\n switch (value) {\n case null:\n return SPECIAL_INTEGERS.NULL;\n case false:\n return SPECIAL_INTEGERS.BOOLEAN_FALSE;\n case true:\n return SPECIAL_INTEGERS.BOOLEAN_TRUE;\n }\n\n const matchedIndex = matchIndex(options, value);\n if (matchedIndex > -1) return matchedIndex;\n\n // value was not listed in options, so we need to\n // transport it using JSON.stringify(). we return 255 to\n // indicate this value is stringified.\n // stringified values will be placed at the end of the\n // indices array\n unlistedValues.push(value);\n return SPECIAL_INTEGERS.UNLISTED_VALUE;\n }),\n );\n\n let joined: Uint8Array;\n // there were unlisted values, so we need to join arrays\n if (unlistedValues.length > 0) {\n const jsonArray = new TextEncoder().encode(\n // slicing removes opening and closing array brackets as they'll always be\n // there and we can re-add them when deserializing\n JSON.stringify(unlistedValues).slice(1, -1),\n );\n joined = joinUint8Arrays(matchedIndices, jsonArray);\n } else {\n joined = matchedIndices;\n }\n\n return memoizedSign(joined, secret);\n}\n"]}