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
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ module.exports = {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"no-shadow": "off", // disable base rule for TypeScript
"@typescript-eslint/no-shadow": ["error"],
"import/prefer-default-export": "off",
"import/extensions": [
"error",
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.0.0-pre.6 - 2025-09-30

- Support extendable typescript types w/typegen from cli

## 0.0.0-pre.5 - 2025-09-24

- Use reforge.com endpoints
Expand Down
15 changes: 10 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { reforge, Reforge, ReforgeBootstrap } from "./src/reforge";
import { reforge, Reforge, ReforgeInitParams, ReforgeBootstrap } from "./src/reforge";
import { Config } from "./src/config";
import Context from "./src/context";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require("./package.json");

export { reforge, Reforge, Config, Context, version };
export { reforge, Reforge, ReforgeInitParams, Config, Context, version };

export { ReforgeBootstrap };

export type { Duration } from "./src/configValue";
export type { default as ConfigValue } from "./src/configValue";
export type { default as ContextValue } from "./src/contextValue";
export type { ConfigValue } from "./src/config";
export type {
Duration,
ContextValue,
Contexts,
TypedFrontEndConfigurationRaw,
FrontEndConfigurationRaw,
} from "./src/types";
export type { CollectContextModeType } from "./src/loader";
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"packageManager": "yarn@4.9.2",
"name": "@reforge-com/javascript",
"version": "0.0.0-pre.5",
"version": "0.0.0-pre.6",
"description": "Feature Flags & Dynamic Configuration as a Service",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
Expand Down
164 changes: 138 additions & 26 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,136 @@
import ConfigKey from "./configKey";
import ConfigValue from "./configValue";
import { ReforgeLogLevel } from "./logger";
import { TypedFrontEndConfigurationRaw, ConfigEvaluationMetadata } from "./types";

export type RawConfigWithoutTypes = { [key: string]: any };

export type ConfigEvaluationMetadata = {
configRowIndex: number;
conditionalValueIndex: number;
type: string;
configId: string;
};
export type RawConfigWithoutTypes = Record<string, any>;

type APIKeyMetadata = {
id: string | number;
};

// TODO: Why is this definition different from the one in ./types.ts?
type Duration = {
definition: string;
millis: number;
};

type Value = {
[key: string]: number | string | string[] | boolean | Duration;
};
export interface IntRange {
/** if empty treat as Number.MIN_VALUE. Inclusive */
start?: bigint | undefined;
/** if empty treat as Number.MAX_VALUE. Exclusive */
end?: bigint | undefined;
}

export enum ProvidedSource {
EnvVar = "ENV_VAR",
}
export interface Provided {
source?: ProvidedSource | undefined;
/** eg MY_ENV_VAR */
lookup?: string | undefined;
}

export enum SchemaType {
UNKNOWN = 0,
ZOD = 1,
JSON_SCHEMA = 2,
}

export interface Schema {
schema: string;
schemaType: SchemaType;
}

export interface WeightedValue {
/** out of 1000 */
weight: number;
// eslint-disable-next-line no-use-before-define
value: ConfigValue | undefined;
}

export enum LimitResponse_LimitPolicyNames {
SecondlyRolling = 1,
MinutelyRolling = 3,
HourlyRolling = 5,
DailyRolling = 7,
MonthlyRolling = 8,
Infinite = 9,
YearlyRolling = 10,
}

export enum LimitDefinition_SafetyLevel {
L4_BEST_EFFORT = 4,
L5_BOMBPROOF = 5,
}

export interface LimitDefinition {
policyName: LimitResponse_LimitPolicyNames;
limit: number;
burst: number;
accountId: number;
lastModified: number;
returnable: boolean;
/** [default = L4_BEST_EFFORT]; // Overridable by request */
safetyLevel: LimitDefinition_SafetyLevel;
}
export interface WeightedValues {
weightedValues: WeightedValue[];
hashByPropertyName?: string | undefined;
}

export type ConfigValue =
| {
int: number | undefined;
}
| {
string: string | undefined;
}
| {
bytes: Buffer | undefined;
}
| {
double: number | undefined;
}
| {
bool: boolean | undefined;
}
| {
weightedValues?: WeightedValues | undefined;
}
| {
limitDefinition?: LimitDefinition | undefined;
}
| {
logLevel: ReforgeLogLevel | undefined;
}
| {
stringList: string[] | undefined;
}
| {
intRange: IntRange | undefined;
}
| {
provided: Provided | undefined;
}
| {
duration: Duration | undefined;
}
| {
json: string | undefined;
}
| {
schema: Schema | undefined;
}
| {
/** don't log or telemetry this value */
confidential: boolean | undefined;
}
| {
/** key name to decrypt with */
decryptWith: string | undefined;
};

type Evaluation = {
value: Value;
value: ConfigValue;
configEvaluationMetadata: {
configRowIndex: string | number;
conditionalValueIndex: string | number;
Expand All @@ -47,7 +153,11 @@ const parseRawMetadata = (metadata: any) => ({
configId: metadata.id,
});

const valueFor = (value: Value, type: string, key: string): ConfigValue => {
const valueFor = <K extends keyof TypedFrontEndConfigurationRaw>(
value: ConfigValue,
type: keyof ConfigValue,
key: K
): TypedFrontEndConfigurationRaw[K] => {
const rawValue = value[type];

switch (type) {
Expand Down Expand Up @@ -77,7 +187,7 @@ export const parseEvaluationPayload = (payload: EvaluationPayload) => {
Object.keys(payload.evaluations).forEach((key) => {
const evaluation = payload.evaluations[key];

const type = Object.keys(evaluation.value)[0];
const type = Object.keys(evaluation.value)[0] as keyof ConfigValue;

// eslint-disable-next-line no-use-before-define
configs[key] = new Config(
Expand All @@ -98,30 +208,32 @@ const parseRawConfigWithoutTypes = (payload: RawConfigWithoutTypes) => {
// eslint-disable-next-line no-use-before-define
const configs = {} as { [key: string]: Config };
Object.keys(payload).forEach((key) => {
const type = typeof payload[key];
const type = typeof payload[key] as keyof ConfigValue;
// eslint-disable-next-line no-use-before-define
configs[key] = new Config(key, valueFor({ [type]: payload[key] }, type, key), type);
});

return configs;
};

export class Config {
key: ConfigKey;
export class Config<
K extends keyof TypedFrontEndConfigurationRaw = keyof TypedFrontEndConfigurationRaw,
> {
key: K;

value: ConfigValue;
value: TypedFrontEndConfigurationRaw[K];

rawValue: Value | undefined;
rawValue: ConfigValue | undefined;

type: string;
type: keyof ConfigValue;

configEvaluationMetadata: ConfigEvaluationMetadata | undefined;

constructor(
key: ConfigKey,
value: ConfigValue,
type: string,
rawValue?: Value,
key: K,
value: TypedFrontEndConfigurationRaw[K],
type: keyof ConfigValue,
rawValue?: ConfigValue,
metadata?: ConfigEvaluationMetadata
) {
this.key = key;
Expand Down
3 changes: 0 additions & 3 deletions src/configKey.ts

This file was deleted.

8 changes: 0 additions & 8 deletions src/configValue.ts

This file was deleted.

5 changes: 1 addition & 4 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import Key from "./key";
import ContextValue from "./contextValue";
import { Contexts, ContextValue } from "./types";
import base64Encode from "./base64Encode";

export type Contexts = { [key: Key]: Record<string, ContextValue> };

const isEqual = (a: Contexts, b: Contexts) => {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
Expand Down
3 changes: 0 additions & 3 deletions src/contextValue.ts

This file was deleted.

15 changes: 8 additions & 7 deletions src/evaluationSummaryAggregator.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import {
massageSelectedValue,
massageConfigForTelemetry,
ConfigEvaluationCounter,
} from "./evaluationSummaryAggregator";
import { Config, parseEvaluationPayload, EvaluationPayload } from "./config";
import { massageSelectedValue, massageConfigForTelemetry } from "./evaluationSummaryAggregator";
import { Config, parseEvaluationPayload, EvaluationPayload, ConfigValue } from "./config";
import { ConfigEvaluationCounter } from "./types";

describe("massageSelectedValue", () => {
[
Expand All @@ -21,7 +18,11 @@ describe("massageSelectedValue", () => {
[{ stringList: ["a", "b", "c"] }, { values: ["a", "b", "c"] }],
].forEach(([value, expected]) => {
it(`should massage ${JSON.stringify(value)} to ${JSON.stringify(expected)}`, () => {
const config = new Config("key", Object.values(value)[0], Object.keys(value)[0]);
const config = new Config(
"key",
Object.values(value)[0],
Object.keys(value)[0] as keyof ConfigValue
);
const massagedValue = massageSelectedValue(config);

expect(massagedValue).toEqual(expected);
Expand Down
35 changes: 8 additions & 27 deletions src/evaluationSummaryAggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,15 @@
// TODO: flush when we receive a config update (or as a result of a context update...but that should trigger a config update anyway)

import { PeriodicSync } from "./periodicSync";
import { Config, ConfigEvaluationMetadata } from "./config";
import { Config } from "./config";
import { type reforge } from "./reforge";

export type ConfigEvaluationCounter = Omit<ConfigEvaluationMetadata, "type"> & {
selectedValue: any;
count: number;
};

type ConfigEvaluationSummary = {
key: string;
type: string; // FEATURE_FLAG, CONFIG, etc
counters: ConfigEvaluationCounter[];
};

type ConfigEvaluationSummaries = {
start: number;
end: number;
summaries: ConfigEvaluationSummary[];
};

type TelemetryEvent = {
summaries: ConfigEvaluationSummaries;
};

type TelemetryEvents = {
instanceHash: string;
events: TelemetryEvent[];
};
import {
ConfigEvaluationCounter,
ConfigEvaluationSummaries,
ConfigEvaluationSummary,
TelemetryEvents,
ConfigEvaluationMetadata,
} from "./types";

export const massageSelectedValue = (config: Config): any => {
if (config.rawValue && (config.type === "duration" || config.type === "json")) {
Expand Down
3 changes: 0 additions & 3 deletions src/exponentialBackoff.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export class ExponentialBackoff {
private initialDelay: number;

private maxDelay: number;

private multiplier: number;
Expand All @@ -9,7 +7,6 @@ export class ExponentialBackoff {

// arguments are in seconds
constructor(maxDelay: number, initialDelay = 2, multiplier = 2) {
this.initialDelay = initialDelay;
this.maxDelay = maxDelay;
this.multiplier = multiplier;
this.delay = initialDelay;
Expand Down
3 changes: 0 additions & 3 deletions src/key.ts

This file was deleted.

Loading