#
tokens: 48750/50000 40/327 files (page 3/7)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 7. Use http://codebase.md/bucketco/bucket-javascript-sdk?lines=false&page={x} to view the full context.

# Directory Structure

```
├── .editorconfig
├── .gitattributes
├── .github
│   └── workflows
│       ├── package-ci.yml
│       └── publish.yml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── .yarnrc.yml
├── docs.sh
├── lerna.json
├── LICENSE
├── package.json
├── packages
│   ├── browser-sdk
│   │   ├── .prettierignore
│   │   ├── eslint.config.js
│   │   ├── example
│   │   │   ├── feedback
│   │   │   │   ├── feedback.html
│   │   │   │   └── Feedback.jsx
│   │   │   └── typescript
│   │   │       ├── app.ts
│   │   │       └── index.html
│   │   ├── FEEDBACK.md
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── postcss.config.js
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── config.ts
│   │   │   ├── context.ts
│   │   │   ├── feedback
│   │   │   │   ├── feedback.ts
│   │   │   │   ├── prompts.ts
│   │   │   │   ├── promptStorage.ts
│   │   │   │   └── ui
│   │   │   │       ├── Button.css
│   │   │   │       ├── Button.tsx
│   │   │   │       ├── config
│   │   │   │       │   └── defaultTranslations.tsx
│   │   │   │       ├── css.d.ts
│   │   │   │       ├── FeedbackDialog.css
│   │   │   │       ├── FeedbackDialog.tsx
│   │   │   │       ├── FeedbackForm.css
│   │   │   │       ├── FeedbackForm.tsx
│   │   │   │       ├── hooks
│   │   │   │       │   └── useTimer.ts
│   │   │   │       ├── index.css
│   │   │   │       ├── index.ts
│   │   │   │       ├── Plug.tsx
│   │   │   │       ├── RadialProgress.css
│   │   │   │       ├── RadialProgress.tsx
│   │   │   │       ├── StarRating.css
│   │   │   │       ├── StarRating.tsx
│   │   │   │       └── types.ts
│   │   │   ├── flag
│   │   │   │   ├── flagCache.ts
│   │   │   │   └── flags.ts
│   │   │   ├── hooksManager.ts
│   │   │   ├── httpClient.ts
│   │   │   ├── index.ts
│   │   │   ├── logger.ts
│   │   │   ├── rateLimiter.ts
│   │   │   ├── sse.ts
│   │   │   ├── toolbar
│   │   │   │   ├── Flags.css
│   │   │   │   ├── Flags.tsx
│   │   │   │   ├── index.css
│   │   │   │   ├── index.ts
│   │   │   │   ├── Switch.css
│   │   │   │   ├── Switch.tsx
│   │   │   │   ├── Toolbar.css
│   │   │   │   └── Toolbar.tsx
│   │   │   └── ui
│   │   │       ├── constants.ts
│   │   │       ├── Dialog.css
│   │   │       ├── Dialog.tsx
│   │   │       ├── icons
│   │   │       │   ├── Check.tsx
│   │   │       │   ├── CheckCircle.tsx
│   │   │       │   ├── Close.tsx
│   │   │       │   ├── Dissatisfied.tsx
│   │   │       │   ├── Logo.tsx
│   │   │       │   ├── Neutral.tsx
│   │   │       │   ├── Satisfied.tsx
│   │   │       │   ├── VeryDissatisfied.tsx
│   │   │       │   └── VerySatisfied.tsx
│   │   │       ├── packages
│   │   │       │   └── floating-ui-preact-dom
│   │   │       │       ├── arrow.ts
│   │   │       │       ├── index.ts
│   │   │       │       ├── README.md
│   │   │       │       ├── types.ts
│   │   │       │       ├── useFloating.ts
│   │   │       │       └── utils
│   │   │       │           ├── deepEqual.ts
│   │   │       │           ├── getDPR.ts
│   │   │       │           ├── roundByDPR.ts
│   │   │       │           └── useLatestRef.ts
│   │   │       ├── types.ts
│   │   │       └── utils.ts
│   │   ├── test
│   │   │   ├── client.test.ts
│   │   │   ├── e2e
│   │   │   │   ├── acceptance.browser.spec.ts
│   │   │   │   ├── empty.html
│   │   │   │   ├── feedback-widget.browser.spec.ts
│   │   │   │   └── give-feedback-button.html
│   │   │   ├── flagCache.test.ts
│   │   │   ├── flags.test.ts
│   │   │   ├── hooksManager.test.ts
│   │   │   ├── httpClient.test.ts
│   │   │   ├── init.test.ts
│   │   │   ├── mocks
│   │   │   │   ├── handlers.ts
│   │   │   │   └── server.ts
│   │   │   ├── prompts.test.ts
│   │   │   ├── promptStorage.test.ts
│   │   │   ├── rateLimiter.test.ts
│   │   │   ├── sse.test.ts
│   │   │   ├── testLogger.ts
│   │   │   └── usage.test.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   ├── typedoc.json
│   │   ├── vite.config.mjs
│   │   ├── vite.e2e.config.js
│   │   └── vitest.setup.ts
│   ├── cli
│   │   ├── .prettierignore
│   │   ├── commands
│   │   │   ├── apps.ts
│   │   │   ├── auth.ts
│   │   │   ├── flags.ts
│   │   │   ├── init.ts
│   │   │   ├── mcp.ts
│   │   │   ├── new.ts
│   │   │   └── rules.ts
│   │   ├── eslint.config.js
│   │   ├── index.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── schema.json
│   │   ├── services
│   │   │   ├── bootstrap.ts
│   │   │   ├── flags.ts
│   │   │   ├── mcp.ts
│   │   │   └── rules.ts
│   │   ├── stores
│   │   │   ├── auth.ts
│   │   │   └── config.ts
│   │   ├── test
│   │   │   └── json.test.ts
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   ├── utils
│   │   │   ├── auth.ts
│   │   │   ├── commander.ts
│   │   │   ├── constants.ts
│   │   │   ├── errors.ts
│   │   │   ├── file.ts
│   │   │   ├── gen.ts
│   │   │   ├── json.ts
│   │   │   ├── options.ts
│   │   │   ├── schemas.ts
│   │   │   ├── types.ts
│   │   │   ├── urls.ts
│   │   │   └── version.ts
│   │   └── vite.config.js
│   ├── eslint-config
│   │   ├── base.js
│   │   └── package.json
│   ├── flag-evaluation
│   │   ├── .prettierignore
│   │   ├── eslint.config.js
│   │   ├── jest.config.js
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   ├── test
│   │   │   └── index.test.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   └── tsconfig.json
│   ├── node-sdk
│   │   ├── .prettierignore
│   │   ├── docs
│   │   │   ├── type-check-failed.png
│   │   │   └── type-check-payload-failed.png
│   │   ├── eslint.config.js
│   │   ├── examples
│   │   │   ├── cloudflare-worker
│   │   │   │   ├── .gitignore
│   │   │   │   ├── .prettierignore
│   │   │   │   ├── .vscode
│   │   │   │   │   └── settings.json
│   │   │   │   ├── package.json
│   │   │   │   ├── README.md
│   │   │   │   ├── src
│   │   │   │   │   └── index.ts
│   │   │   │   ├── tsconfig.json
│   │   │   │   ├── vitest.config.mts
│   │   │   │   ├── worker-configuration.d.ts
│   │   │   │   ├── wrangler.jsonc
│   │   │   │   └── yarn.lock
│   │   │   └── express
│   │   │       ├── app.test.ts
│   │   │       ├── app.ts
│   │   │       ├── bucket.ts
│   │   │       ├── bucketConfig.json
│   │   │       ├── package.json
│   │   │       ├── README.md
│   │   │       ├── serve.ts
│   │   │       ├── tsconfig.json
│   │   │       └── yarn.lock
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── batch-buffer.ts
│   │   │   ├── client.ts
│   │   │   ├── config.ts
│   │   │   ├── edgeClient.ts
│   │   │   ├── fetch-http-client.ts
│   │   │   ├── flusher.ts
│   │   │   ├── index.ts
│   │   │   ├── inRequestCache.ts
│   │   │   ├── periodicallyUpdatingCache.ts
│   │   │   ├── rate-limiter.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── test
│   │   │   ├── batch-buffer.test.ts
│   │   │   ├── client.test.ts
│   │   │   ├── config.test.ts
│   │   │   ├── fetch-http-client.test.ts
│   │   │   ├── flusher.test.ts
│   │   │   ├── inRequestCache.test.ts
│   │   │   ├── periodicallyUpdatingCache.test.ts
│   │   │   ├── rate-limiter.test.ts
│   │   │   ├── testConfig.json
│   │   │   └── utils.test.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   ├── typedoc.json
│   │   └── vite.config.js
│   ├── openfeature-browser-provider
│   │   ├── .prettierignore
│   │   ├── eslint.config.js
│   │   ├── example
│   │   │   ├── .eslintrc.json
│   │   │   ├── .gitignore
│   │   │   ├── app
│   │   │   │   ├── featureManagement.ts
│   │   │   │   ├── globals.css
│   │   │   │   ├── layout.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── components
│   │   │   │   ├── Context.tsx
│   │   │   │   ├── HuddleFeature.tsx
│   │   │   │   └── OpenFeatureProvider.tsx
│   │   │   ├── next.config.mjs
│   │   │   ├── package.json
│   │   │   ├── postcss.config.mjs
│   │   │   ├── README.md
│   │   │   ├── tailwind.config.ts
│   │   │   └── tsconfig.json
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   └── vite.config.js
│   ├── openfeature-node-provider
│   │   ├── .prettierignore
│   │   ├── eslint.config.js
│   │   ├── example
│   │   │   ├── app.ts
│   │   │   ├── package.json
│   │   │   ├── README.md
│   │   │   ├── reflag.ts
│   │   │   ├── serve.ts
│   │   │   ├── tsconfig.json
│   │   │   └── yarn.lock
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   └── vite.config.js
│   ├── react-sdk
│   │   ├── .prettierignore
│   │   ├── dev
│   │   │   ├── .env
│   │   │   ├── nextjs-bootstrap-demo
│   │   │   │   ├── .eslintrc.json
│   │   │   │   ├── .gitignore
│   │   │   │   ├── app
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── favicon.ico
│   │   │   │   │   ├── globals.css
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── components
│   │   │   │   │   └── Flags.tsx
│   │   │   │   ├── next.config.mjs
│   │   │   │   ├── package.json
│   │   │   │   ├── postcss.config.mjs
│   │   │   │   ├── public
│   │   │   │   │   ├── next.svg
│   │   │   │   │   └── vercel.svg
│   │   │   │   ├── README.md
│   │   │   │   ├── tailwind.config.ts
│   │   │   │   └── tsconfig.json
│   │   │   ├── nextjs-flag-demo
│   │   │   │   ├── .eslintrc.json
│   │   │   │   ├── .gitignore
│   │   │   │   ├── app
│   │   │   │   │   ├── favicon.ico
│   │   │   │   │   ├── globals.css
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── components
│   │   │   │   │   ├── Flags.tsx
│   │   │   │   │   └── Providers.tsx
│   │   │   │   ├── next.config.mjs
│   │   │   │   ├── package.json
│   │   │   │   ├── postcss.config.mjs
│   │   │   │   ├── public
│   │   │   │   │   ├── next.svg
│   │   │   │   │   └── vercel.svg
│   │   │   │   ├── README.md
│   │   │   │   ├── tailwind.config.ts
│   │   │   │   └── tsconfig.json
│   │   │   └── plain
│   │   │       ├── app.tsx
│   │   │       ├── index.html
│   │   │       ├── index.tsx
│   │   │       ├── tsconfig.json
│   │   │       └── vite-env.d.ts
│   │   ├── eslint.config.js
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   └── index.tsx
│   │   ├── test
│   │   │   └── usage.test.tsx
│   │   ├── tsconfig.build.json
│   │   ├── tsconfig.eslint.json
│   │   ├── tsconfig.json
│   │   ├── typedoc.json
│   │   └── vite.config.mjs
│   ├── tsconfig
│   │   ├── library.json
│   │   └── package.json
│   └── vue-sdk
│       ├── .prettierignore
│       ├── dev
│       │   └── plain
│       │       ├── App.vue
│       │       ├── components
│       │       │   ├── Events.vue
│       │       │   ├── FlagsList.vue
│       │       │   ├── MissingKeyMessage.vue
│       │       │   ├── RequestFeedback.vue
│       │       │   ├── Section.vue
│       │       │   ├── StartHuddlesButton.vue
│       │       │   └── Track.vue
│       │       ├── env.d.ts
│       │       ├── index.html
│       │       └── index.ts
│       ├── eslint.config.js
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── hooks.ts
│       │   ├── index.ts
│       │   ├── ReflagBootstrappedProvider.vue
│       │   ├── ReflagClientProvider.vue
│       │   ├── ReflagProvider.vue
│       │   ├── types.ts
│       │   ├── version.ts
│       │   └── vue.d.ts
│       ├── test
│       │   └── usage.test.ts
│       ├── tsconfig.build.json
│       ├── tsconfig.eslint.json
│       ├── tsconfig.json
│       ├── typedoc.json
│       └── vite.config.mjs
├── README.md
├── typedoc.json
├── vitest.workspace.js
└── yarn.lock
```

# Files

--------------------------------------------------------------------------------
/packages/node-sdk/src/fetch-http-client.ts:
--------------------------------------------------------------------------------

```typescript
import { API_TIMEOUT_MS } from "./config";
import { HttpClient } from "./types";
import { ok } from "./utils";

/**
 * The default HTTP client implementation.
 *
 * @remarks
 * This implementation uses the `fetch` API to send HTTP requests.
 **/
const fetchClient: HttpClient = {
  post: async <TBody, TResponse>(
    url: string,
    headers: Record<string, string>,
    body: TBody,
    timeoutMs: number = API_TIMEOUT_MS,
  ) => {
    ok(typeof url === "string" && url.length > 0, "URL must be a string");
    ok(typeof headers === "object", "Headers must be an object");

    const response = await fetch(url, {
      method: "post",
      headers,
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(timeoutMs),
    });

    const json = await response.json();
    return {
      ok: response.ok,
      status: response.status,
      body: json as TResponse,
    };
  },

  get: async <TResponse>(
    url: string,
    headers: Record<string, string>,
    timeoutMs: number = API_TIMEOUT_MS,
  ) => {
    ok(typeof url === "string" && url.length > 0, "URL must be a string");
    ok(typeof headers === "object", "Headers must be an object");

    const response = await fetch(url, {
      method: "get",
      headers,
      signal: AbortSignal.timeout(timeoutMs),
      // We must use no-cache to avoid services such as Next.js from caching the response indefinitely.
      // We also can't use no-store because of Next.js error withRetry https://github.com/vercel/next.js/discussions/54036.
      // We also have local caching in the SDKs, so we don't need to cache the response.
      cache: "no-cache",
    });

    const json = await response.json();
    return {
      ok: response.ok,
      status: response.status,
      body: json as TResponse,
    };
  },
};

/**
 * Implements exponential backoff retry logic for async functions.
 *
 * @param fn - The async function to retry.
 * @param maxRetries - Maximum number of retry attempts.
 * @param baseDelay - Base delay in milliseconds before retrying.
 * @param maxDelay - Maximum delay in milliseconds.
 * @returns The result of the function call or throws an error if all retries fail.
 */
export async function withRetry<T>(
  fn: () => Promise<T>,
  onFailedTry: (error: unknown) => void,
  maxRetries: number,
  baseDelay: number,
  maxDelay: number,
): Promise<T> {
  let lastError: unknown;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      if (attempt === maxRetries) {
        break;
      }

      onFailedTry(error);

      // Calculate exponential backoff with jitter
      const delay = Math.min(
        maxDelay,
        baseDelay * Math.pow(2, attempt) * (0.8 + Math.random() * 0.4),
      );

      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

export default fetchClient;

```

--------------------------------------------------------------------------------
/packages/vue-sdk/test/usage.test.ts:
--------------------------------------------------------------------------------

```typescript
import { mount } from "@vue/test-utils";
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
import { defineComponent, h, nextTick } from "vue";

import { ReflagClient } from "@reflag/browser-sdk";

import {
  ReflagBootstrappedProvider,
  ReflagProvider,
  useClient,
  useFlag,
} from "../src";

// Mock ReflagClient prototype methods like the React SDK tests
beforeAll(() => {
  vi.spyOn(ReflagClient.prototype, "initialize").mockResolvedValue(undefined);
  vi.spyOn(ReflagClient.prototype, "stop").mockResolvedValue(undefined);
  vi.spyOn(ReflagClient.prototype, "getFlag").mockReturnValue({
    isEnabled: true,
    config: { key: "default", payload: { message: "Hello" } },
    track: vi.fn().mockResolvedValue(undefined),
    requestFeedback: vi.fn(),
    setIsEnabledOverride: vi.fn(),
    isEnabledOverride: null,
  });
  vi.spyOn(ReflagClient.prototype, "getFlags").mockReturnValue({});
  vi.spyOn(ReflagClient.prototype, "on").mockReturnValue(() => {
    // cleanup function
  });
  vi.spyOn(ReflagClient.prototype, "off").mockImplementation(() => {
    // off implementation
  });
});

beforeEach(() => {
  vi.clearAllMocks();
});

function getProvider() {
  return {
    props: {
      publishableKey: "key",
    },
  };
}

describe("ReflagProvider", () => {
  test("provides the client", async () => {
    const Child = defineComponent({
      setup() {
        const client = useClient();
        return { client };
      },
      template: "<div></div>",
    });

    const wrapper = mount(ReflagProvider, {
      ...getProvider(),
      slots: { default: () => h(Child) },
    });

    await nextTick();
    expect(wrapper.findComponent(Child).vm.client).toBeDefined();
  });

  test("throws without provider", () => {
    const Comp = defineComponent({
      setup() {
        return () => {
          useClient();
        };
      },
    });

    expect(() => mount(Comp)).toThrow();
  });
});

describe("ReflagBootstrappedProvider", () => {
  test("provides the client with bootstrapped flags", async () => {
    const bootstrappedFlags = {
      context: {
        user: { id: "test-user" },
        company: { id: "test-company" },
      },
      flags: {
        "test-flag": {
          key: "test-flag",
          isEnabled: true,
          config: { key: "default", payload: { message: "Hello" } },
        },
      },
    };

    const Child = defineComponent({
      setup() {
        const client = useClient();
        const flag = useFlag("test-flag");
        return { client, flag };
      },
      template: "<div></div>",
    });

    const wrapper = mount(ReflagBootstrappedProvider, {
      props: {
        publishableKey: "key",
        flags: bootstrappedFlags,
      },
      slots: { default: () => h(Child) },
    });

    await nextTick();
    expect(wrapper.findComponent(Child).vm.client).toBeDefined();
    expect(wrapper.findComponent(Child).vm.flag.isEnabled.value).toBe(true);
  });
});

```

--------------------------------------------------------------------------------
/packages/cli/commands/init.ts:
--------------------------------------------------------------------------------

```typescript
import { input, select } from "@inquirer/prompts";
import chalk from "chalk";
import { Command } from "commander";
import { relative } from "node:path";
import ora, { Ora } from "ora";

import { App, listApps } from "../services/bootstrap.js";
import { configStore, typeFormats } from "../stores/config.js";
import { DEFAULT_TYPES_OUTPUT } from "../utils/constants.js";
import { handleError } from "../utils/errors.js";
import { overwriteOption } from "../utils/options.js";

type InitArgs = {
  overwrite?: boolean;
};

export const initAction = async (args: InitArgs = {}) => {
  let spinner: Ora | undefined;
  let apps: App[] = [];

  try {
    // Check if config already exists
    const configPath = configStore.getConfigPath();
    if (configPath && !args.overwrite) {
      throw new Error(
        "Reflag is already initialized. Use --overwrite to overwrite.",
      );
    }

    console.log("\nWelcome to ◪ Reflag!\n");
    const baseUrl = configStore.getConfig("baseUrl");

    // Load apps
    spinner = ora(`Loading apps from ${chalk.cyan(baseUrl)}...`).start();
    apps = listApps();
    spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}.`);
  } catch (error) {
    spinner?.fail("Loading apps failed.");
    handleError(error, "Initialization");
  }

  try {
    let appId: string | undefined;
    const nonDemoApp = apps.find((app) => !app.demo);

    if (apps.length === 0) {
      throw new Error("You don't have any apps yet. Please create one.");
    } else {
      const longestName = Math.max(...apps.map((app) => app.name.length));
      appId = await select({
        message: "Select an app",
        default: nonDemoApp?.id,
        choices: apps.map((app) => ({
          name: `${app.name.padEnd(longestName, " ")}${app.demo ? " [Demo]" : ""}`,
          value: app.id,
        })),
      });
    }

    // Get types output path
    const typesOutput = await input({
      message: "Where should we generate the types?",
      default: DEFAULT_TYPES_OUTPUT,
    });

    // Get types output format
    const typesFormat = await select({
      message: "What is the output format?",
      choices: typeFormats.map((format) => ({
        name: format,
        value: format,
      })),
      default: "react",
    });

    // Update config
    configStore.setConfig({
      appId,
      typesOutput: [{ path: typesOutput, format: typesFormat }],
    });

    // Create config file
    spinner = ora("Creating configuration...").start();
    await configStore.saveConfigFile(args.overwrite);

    spinner.succeed(
      `Configuration created at ${chalk.cyan(relative(process.cwd(), configStore.getConfigPath()!))}.`,
    );
  } catch (error) {
    spinner?.fail("Configuration creation failed.");
    handleError(error, "Initialization");
  }
};

export function registerInitCommand(cli: Command) {
  cli
    .command("init")
    .description("Initialize a new Reflag configuration.")
    .addOption(overwriteOption)
    .action(initAction);
}

```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/FeedbackForm.css:
--------------------------------------------------------------------------------

```css
.container {
  overflow-y: hidden;
  transition: max-height 400ms cubic-bezier(0.65, 0, 0.35, 1);
}

.form {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  overflow-y: hidden;
  max-height: 400px;
  transition: opacity 400ms cubic-bezier(0.65, 0, 0.35, 1);
}

.form-control {
  display: flex;
  flex-direction: column;
  width: 100%;
  gap: 8px;
  border: none;
  padding: 0;
  margin: 0;

  font-size: 12px;
  color: var(--reflag-feedback-dialog-secondary-color, #787c91);
}

.form-expanded-content {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  transition: opacity 400ms cubic-bezier(0.65, 0, 0.35, 1);

  opacity: 0;
  position: absolute;
  top: 0;
  left: 0;
}

.title {
  color: var(--reflag-feedback-dialog-color, #1e1f24);
  font-size: 15px;
  font-weight: 400;
  line-height: 115%;
  text-wrap: balance;
  max-width: calc(100% - 20px);
  margin-bottom: 6px;
  line-height: 1.3;
}

.dimmed {
  opacity: 0.5;
}

.textarea {
  background-color: transparent;
  border: 1px solid;
  border-color: var(--reflag-feedback-dialog-input-border-color, #d8d9df);
  padding: 0.5rem 0.75rem;
  border-radius: var(--reflag-feedback-dialog-border-radius, 6px);
  transition: border-color 0.2s ease-in-out;
  font-family: var(
    --reflag-feedback-dialog-font-family,
    InterVariable,
    Inter,
    system-ui,
    Open Sans,
    sans-serif
  );
  line-height: 1.3;
  resize: none;

  color: var(--reflag-feedback-dialog-color, #1e1f24);
  font-size: 13px;

  &::placeholder {
    color: var(--reflag-feedback-dialog-color, #1e1f24);
    opacity: 0.36;
  }

  &:focus {
    outline: none;
    border-color: var(
      --reflag-feedback-dialog-input-focus-border-color,
      #787c91
    );
  }
}

.score-status-container {
  position: relative;
  padding-bottom: 6px;
  height: 14px;

  > .score-status {
    display: flex;
    align-items: center;

    position: absolute;
    top: 0;
    left: 0;

    opacity: 0;
    transition: opacity 200ms ease-in-out;
  }
}

.error {
  margin: 0;
  color: var(--reflag-feedback-dialog-error-color, #e53e3e);
  font-size: 0.8125em;
  font-weight: 500;
}

.submitted {
  display: flex;
  flex-direction: column;
  transition: opacity 400ms cubic-bezier(0.65, 0, 0.35, 1);

  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  pointer-events: none;
  width: calc(100% - 56px);

  padding: 0px 28px;

  .submitted-check {
    background: var(--reflag-feedback-dialog-submitted-check-color, #fff);
    color: var(
      --reflag-feedback-dialog-submitted-check-background-color,
      #38a169
    );
    height: 24px;
    width: 24px;
    display: block;
    border-radius: 50%;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;

    margin: 16px auto 8px;
  }

  .text {
    margin: auto auto 16px;
    text-align: center;
    color: var(--reflag-feedback-dialog-color, #1e1f24);
    font-size: var(--reflag-feedback-dialog-font-size, 1rem);
    font-weight: 400;
    line-height: 130%;

    flex-grow: 1;
    max-width: 160px;
  }

  > .plug {
    flex-grow: 0;
  }
}

```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx:
--------------------------------------------------------------------------------

```typescript
import { Fragment, FunctionComponent, h } from "preact";
import { useCallback, useState } from "preact/hooks";

import { feedbackContainerId } from "../../ui/constants";
import { Dialog, useDialog } from "../../ui/Dialog";
import { Close } from "../../ui/icons/Close";

import { DEFAULT_TRANSLATIONS } from "./config/defaultTranslations";
import { useTimer } from "./hooks/useTimer";
import { FeedbackForm } from "./FeedbackForm";
import styles from "./index.css?inline";
import { RadialProgress } from "./RadialProgress";
import {
  FeedbackScoreSubmission,
  FeedbackSubmission,
  OpenFeedbackFormOptions,
  WithRequired,
} from "./types";

export type FeedbackDialogProps = WithRequired<
  OpenFeedbackFormOptions,
  "onSubmit" | "position"
>;

const INACTIVE_DURATION_MS = 20 * 1000;
const SUCCESS_DURATION_MS = 3 * 1000;

export const FeedbackDialog: FunctionComponent<FeedbackDialogProps> = ({
  key,
  title = DEFAULT_TRANSLATIONS.DefaultQuestionLabel,
  position,
  translations = DEFAULT_TRANSLATIONS,
  openWithCommentVisible = false,
  onClose,
  onDismiss,
  onSubmit,
  onScoreSubmit,
}) => {
  const [feedbackId, setFeedbackId] = useState<string | undefined>(undefined);
  const [scoreState, setScoreState] = useState<
    "idle" | "submitting" | "submitted"
  >("idle");

  const { isOpen, close } = useDialog({ onClose, initialValue: true });

  const autoClose = useTimer({
    enabled: position.type === "DIALOG",
    initialDuration: INACTIVE_DURATION_MS,
    onEnd: close,
  });

  const submit = useCallback(
    async (data: Omit<FeedbackSubmission, "feedbackId">) => {
      await onSubmit({ ...data, feedbackId });
      autoClose.startWithDuration(SUCCESS_DURATION_MS);
    },
    [autoClose, feedbackId, onSubmit],
  );

  const submitScore = useCallback(
    async (data: Omit<FeedbackScoreSubmission, "feedbackId">) => {
      if (onScoreSubmit !== undefined) {
        setScoreState("submitting");

        const res = await onScoreSubmit({ ...data, feedbackId });
        setFeedbackId(res.feedbackId);
        setScoreState("submitted");
      }
    },
    [feedbackId, onScoreSubmit],
  );
  const dismiss = useCallback(() => {
    autoClose.stop();
    close();
    onDismiss?.();
  }, [autoClose, close, onDismiss]);

  return (
    <>
      <style dangerouslySetInnerHTML={{ __html: styles }} />
      <Dialog
        key={key}
        close={close}
        containerId={feedbackContainerId}
        isOpen={isOpen}
        position={position}
        onDismiss={onDismiss}
      >
        <>
          <FeedbackForm
            key={key}
            openWithCommentVisible={openWithCommentVisible}
            question={title}
            scoreState={scoreState}
            t={{ ...DEFAULT_TRANSLATIONS, ...translations }}
            onInteraction={autoClose.stop}
            onScoreSubmit={submitScore}
            onSubmit={submit}
          />

          <button class="close" onClick={dismiss}>
            {!autoClose.stopped && autoClose.elapsedFraction > 0 && (
              <RadialProgress
                diameter={28}
                progress={1.0 - autoClose.elapsedFraction}
              />
            )}
            <Close />
          </button>
        </>
      </Dialog>
    </>
  );
};

```

--------------------------------------------------------------------------------
/packages/browser-sdk/test/e2e/acceptance.browser.spec.ts:
--------------------------------------------------------------------------------

```typescript
import { randomUUID } from "crypto";
import { expect, test } from "@playwright/test";

import { API_BASE_URL } from "../../src/config";

const KEY = randomUUID();

test("Acceptance", async ({ page }) => {
  await page.goto("http://localhost:8001/test/e2e/empty.html");

  const successfulRequests: string[] = [];

  // Mock API calls with assertions
  await page.route(`${API_BASE_URL}/features/evaluated*`, async (route) => {
    successfulRequests.push("FLAGS");
    await route.fulfill({
      status: 200,
      body: JSON.stringify({
        success: true,
        features: {},
      }),
    });
  });

  await page.route(`${API_BASE_URL}/user`, async (route) => {
    expect(route.request().method()).toEqual("POST");
    expect(route.request().postDataJSON()).toMatchObject({
      userId: "foo",
      attributes: {
        name: "john doe",
      },
    });

    successfulRequests.push("USER");
    await route.fulfill({
      status: 200,
      body: JSON.stringify({ success: true }),
    });
  });

  await page.route(`${API_BASE_URL}/company`, async (route) => {
    expect(route.request().method()).toEqual("POST");
    expect(route.request().postDataJSON()).toMatchObject({
      userId: "foo",
      companyId: "bar",
      attributes: {
        name: "bar corp",
      },
    });

    successfulRequests.push("COMPANY");
    await route.fulfill({
      status: 200,
      body: JSON.stringify({ success: true }),
    });
  });

  await page.route(`${API_BASE_URL}/event`, async (route) => {
    expect(route.request().method()).toEqual("POST");
    expect(route.request().postDataJSON()).toMatchObject({
      userId: "foo",
      companyId: "bar",
      event: "baz",
      attributes: {
        baz: true,
      },
    });

    successfulRequests.push("EVENT");
    await route.fulfill({
      status: 200,
      body: JSON.stringify({ success: true }),
    });
  });

  await page.route(`${API_BASE_URL}/feedback`, async (route) => {
    expect(route.request().method()).toEqual("POST");
    expect(route.request().postDataJSON()).toMatchObject({
      userId: "foo",
      companyId: "bar",
      featureId: "featureId1",
      score: 5,
      comment: "test!",
      question: "actual question",
      promptedQuestion: "prompted question",
    });

    successfulRequests.push("FEEDBACK");
    await route.fulfill({
      status: 200,
      body: JSON.stringify({ success: true }),
    });
  });

  // Golden path requests
  await page.evaluate(`
    ;(async () => {
    const { ReflagClient } = await import("/dist/reflag-browser-sdk.mjs");
      const reflagClient = new ReflagClient({
        publishableKey: "${KEY}",
        user: {
          id: "foo",
          name: "john doe",
        },
        company: {
          id: "bar",
          name: "bar corp",
        }
      });
      await reflagClient.initialize();
      await reflagClient.track("baz", { baz: true }, "foo", "bar");
      await reflagClient.feedback({
        featureId: "featureId1",
        score: 5,
        comment: "test!",
        question: "actual question",
        promptedQuestion: "prompted question",
      });
    })()
  `);

  // Assert all API requests were made
  expect(successfulRequests).toEqual([
    "FLAGS",
    "USER",
    "COMPANY",
    "EVENT",
    "FEEDBACK",
  ]);
});

```

--------------------------------------------------------------------------------
/packages/vue-sdk/dev/plain/components/FlagsList.vue:
--------------------------------------------------------------------------------

```vue
<template>
  <Section title="Flags List">
    <div v-if="!client">
      <p>Client not available</p>
    </div>
    <div v-else>
      <p>This list shows all available flags and their current state:</p>
      <ul
        v-if="flagEntries.length > 0"
        style="list-style-type: none; padding: 0"
      >
        <li
          v-for="[flagKey, flag] in flagEntries"
          :key="flagKey"
          style="
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
          "
        >
          <div style="display: flex; align-items: center; gap: 10px">
            <strong>{{ flagKey }}</strong>
            <span
              :style="{
                color:
                  (flag.isEnabledOverride ?? flag.isEnabled) ? 'green' : 'red',
              }"
            >
              {{
                (flag.isEnabledOverride ?? flag.isEnabled)
                  ? "Enabled"
                  : "Disabled"
              }}
            </span>

            <!-- Reset button if override is active -->
            <button
              v-if="flag.isEnabledOverride !== null"
              style="margin-left: 10px; padding: 2px 8px; font-size: 12px"
              @click="() => resetOverride(flagKey)"
            >
              Reset
            </button>

            <!-- Toggle checkbox -->
            <input
              type="checkbox"
              :checked="flag.isEnabledOverride ?? flag.isEnabled"
              style="margin-left: auto"
              @change="
                (e) => {
                  const isChecked = (e.target as HTMLInputElement).checked;
                  const isEnabledOverride = flag.isEnabledOverride !== null;
                  toggleFlag(flagKey, !isEnabledOverride ? isChecked : null);
                }
              "
            />
          </div>

          <!-- Show config if available -->
          <div
            v-if="flag.config && flag.config.key"
            style="margin-top: 5px; font-size: 12px; color: #666"
          >
            <strong>Config:</strong>
            <pre
              style="
                margin: 2px 0;
                padding: 4px;
                background: #f5f5f5;
                border-radius: 2px;
                overflow: auto;
              "
              >{{ JSON.stringify(flag.config.payload, null, 2) }}</pre
            >
          </div>
        </li>
      </ul>
      <p v-else style="color: #666; font-style: italic">No flags available</p>
    </div>
  </Section>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";

import { useClient, useOnEvent } from "../../../src";

import Section from "./Section.vue";

const client = useClient();
const flagsData = ref(client.getFlags());

// Update flags data when flags are updated
function updateFlags() {
  flagsData.value = client.getFlags();
}

// Update flags data when flags are updated
useOnEvent("flagsUpdated", updateFlags);

const flagEntries = computed(() => {
  return Object.entries(flagsData.value);
});

function resetOverride(flagKey: string) {
  client.getFlag(flagKey).setIsEnabledOverride(null);
  updateFlags();
}

function toggleFlag(flagKey: string, checked: boolean | null) {
  // Use simplified logic similar to React implementation
  client.getFlag(flagKey).setIsEnabledOverride(checked);
  updateFlags();
}
</script>

```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/packages/floating-ui-preact-dom/types.ts:
--------------------------------------------------------------------------------

```typescript
import type {
  ComputePositionConfig,
  ComputePositionReturn,
  VirtualElement,
} from "@floating-ui/dom";
import { h, RefObject } from "preact";

export { arrow, Options as ArrowOptions } from "./arrow";
export { useFloating } from "./useFloating";
export type {
  AlignedPlacement,
  Alignment,
  AutoPlacementOptions,
  AutoUpdateOptions,
  Axis,
  Boundary,
  ClientRectObject,
  ComputePositionConfig,
  ComputePositionReturn,
  Coords,
  DetectOverflowOptions,
  Dimensions,
  ElementContext,
  ElementRects,
  Elements,
  FlipOptions,
  FloatingElement,
  HideOptions,
  InlineOptions,
  Length,
  Middleware,
  MiddlewareArguments,
  MiddlewareData,
  MiddlewareReturn,
  MiddlewareState,
  NodeScroll,
  OffsetOptions,
  Padding,
  Placement,
  Platform,
  Rect,
  ReferenceElement,
  RootBoundary,
  ShiftOptions,
  Side,
  SideObject,
  SizeOptions,
  Strategy,
  VirtualElement,
} from "@floating-ui/dom";
export {
  autoPlacement,
  autoUpdate,
  computePosition,
  detectOverflow,
  flip,
  getOverflowAncestors,
  hide,
  inline,
  limitShift,
  offset,
  platform,
  shift,
  size,
} from "@floating-ui/dom";

type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

export type UseFloatingData = Prettify<
  ComputePositionReturn & { isPositioned: boolean }
>;

export type ReferenceType = Element | VirtualElement;

export type UseFloatingReturn<RT extends ReferenceType = ReferenceType> =
  Prettify<
    UseFloatingData & {
      /**
       * Update the position of the floating element, re-rendering the component
       * if required.
       */
      update: () => void;
      /**
       * Pre-configured positioning styles to apply to the floating element.
       */
      floatingStyles: h.JSX.CSSProperties;
      /**
       * Object containing the reference and floating refs and reactive setters.
       */
      refs: {
        /**
         * A React ref to the reference element.
         */
        reference: RefObject<RT | null>;
        /**
         * A React ref to the floating element.
         */
        floating: RefObject<HTMLElement | null>;
        /**
         * A callback to set the reference element (reactive).
         */
        setReference: (node: RT | null) => void;
        /**
         * A callback to set the floating element (reactive).
         */
        setFloating: (node: HTMLElement | null) => void;
      };
      elements: {
        reference: RT | null;
        floating: HTMLElement | null;
      };
    }
  >;

export type UseFloatingOptions<RT extends ReferenceType = ReferenceType> =
  Prettify<
    Partial<ComputePositionConfig> & {
      /**
       * A callback invoked when both the reference and floating elements are
       * mounted, and cleaned up when either is unmounted. This is useful for
       * setting up event listeners (e.g. pass `autoUpdate`).
       */
      whileElementsMounted?: (
        reference: RT,
        floating: HTMLElement,
        update: () => void,
      ) => () => void;
      elements?: {
        reference?: RT | null;
        floating?: HTMLElement | null;
      };
      /**
       * The `open` state of the floating element to synchronize with the
       * `isPositioned` value.
       */
      open?: boolean;
      /**
       * Whether to use `transform` for positioning instead of `top` and `left`
       * (layout) in the `floatingStyles` object.
       */
      transform?: boolean;
    }
  >;

```

--------------------------------------------------------------------------------
/packages/openfeature-node-provider/example/app.ts:
--------------------------------------------------------------------------------

```typescript
import express from "express";
import "./reflag";
import { EvaluationContext, OpenFeature } from "@openfeature/server-sdk";
import { CreateTodosConfig } from "./reflag";

// In the following, we assume that targetingKey is a unique identifier for the user.
type Context = EvaluationContext & {
  targetingKey: string;
  companyId: string;
};

// Augment the Express types to include the some context property on the `res.locals` object.
declare global {
  namespace Express {
    interface Locals {
      context: Context;
    }
  }
}

const app = express();

app.use(express.json());

app.use((req, res, next) => {
  const ofContext = {
    targetingKey: "user42",
    companyId: "company99",
  };
  res.locals.context = ofContext;
  next();
});

const todos = ["Buy milk", "Walk the dog"];

app.get("/", (_req, res) => {
  const ofClient = OpenFeature.getClient();
  ofClient.track("front-page-viewed", res.locals.context);

  res.json({ message: "Ready to manage some TODOs!" });
});

app.get("/todos", async (req, res) => {
  // Return todos if the feature is enabled for the user
  // We use the `getFlag` method to check if the user has the "show-todo" feature enabled.
  // Note that "show-todo" is a flag that we defined in the `Flags` interface in the `reflag.ts` file.
  // and that the indexing for flag name below is type-checked at compile time.
  const ofClient = OpenFeature.getClient();
  const isEnabled = await ofClient.getBooleanValue(
    "show-todos",
    false,
    res.locals.context,
  );

  if (isEnabled) {
    ofClient.track("show-todo", res.locals.context);
    return res.json({ todos });
  }

  return res
    .status(403)
    .json({ error: "You do not have access to this feature yet!" });
});

app.post("/todos", async (req, res) => {
  const { todo } = req.body;

  if (typeof todo !== "string") {
    return res.status(400).json({ error: "Invalid todo" });
  }

  const ofClient = OpenFeature.getClient();
  const isEnabled = await ofClient.getBooleanValue(
    "create-todo",
    false,
    res.locals.context,
  );

  // Check if the user has the "create-todos" feature enabled.
  if (isEnabled) {
    // Get the configuration for the "create-todos" feature.
    // We expect the configuration to be a JSON object with a `maxLength` property.
    const config = await ofClient.getObjectValue<CreateTodosConfig>(
      "create-todos",
      { maxLength: 100 },
      res.locals.context,
    );

    // Check if the todo is too long.
    if (todo.length > config.maxLength) {
      return res.status(400).json({ error: "Todo is too long" });
    }

    // Track the feature usage
    ofClient.track("create-todos", res.locals.context);
    todos.push(todo);

    return res.status(201).json({ todo });
  }

  res
    .status(403)
    .json({ error: "You do not have access to this feature yet!" });
});

app.delete("/todos/:idx", async (req, res) => {
  const idx = parseInt(req.params.idx);

  if (isNaN(idx) || idx < 0 || idx >= todos.length) {
    return res.status(400).json({ error: "Invalid index" });
  }

  const ofClient = OpenFeature.getClient();
  const isEnabled = await ofClient.getBooleanValue(
    "delete-todos",
    false,
    res.locals.context,
  );

  if (isEnabled) {
    todos.splice(idx, 1);

    ofClient.track("delete-todos", res.locals.context);
    return res.json({});
  }

  res
    .status(403)
    .json({ error: "You do not have access to this feature yet!" });
});

export default app;

```

--------------------------------------------------------------------------------
/packages/node-sdk/test/rate-limiter.test.ts:
--------------------------------------------------------------------------------

```typescript
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";

import { newRateLimiter } from "../src/rate-limiter";

describe("rateLimiter", () => {
  beforeAll(() => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
  });

  afterAll(() => {
    vi.useRealTimers();
  });

  const windowSizeMs = 1000;

  describe("isAllowed", () => {
    it("should rate limit", () => {
      const limiter = newRateLimiter(windowSizeMs);

      expect(limiter.isAllowed("key")).toBe(true);
      expect(limiter.isAllowed("key")).toBe(false);
    });

    it("should reset the limit in given time", () => {
      const limiter = newRateLimiter(windowSizeMs);

      limiter.isAllowed("key");

      vi.advanceTimersByTime(windowSizeMs);
      expect(limiter.isAllowed("key")).toBe(false);

      vi.advanceTimersByTime(1);
      expect(limiter.isAllowed("key")).toBe(true);
    });

    it("should measure events separately by key", () => {
      const limiter = newRateLimiter(windowSizeMs);

      expect(limiter.isAllowed("key1")).toBe(true);

      vi.advanceTimersByTime(windowSizeMs);
      expect(limiter.isAllowed("key2")).toBe(true);
      expect(limiter.isAllowed("key1")).toBe(false);

      vi.advanceTimersByTime(1);
      expect(limiter.isAllowed("key1")).toBe(true);

      vi.advanceTimersByTime(windowSizeMs);
      expect(limiter.isAllowed("key2")).toBe(true);
    });
  });

  describe("clearStale", () => {
    it("should clear expired events, but keep non-expired", () => {
      const rateLimiter = newRateLimiter(windowSizeMs);
      rateLimiter.isAllowed("key1");
      expect(rateLimiter.cacheSize()).toBe(1);

      vi.advanceTimersByTime(windowSizeMs / 2); // 500ms
      rateLimiter.isAllowed("key2");
      expect(rateLimiter.cacheSize()).toBe(2);

      vi.advanceTimersByTime(windowSizeMs / 2 + 1); // 1001ms total
      // at this point, key1 is stale, but key2 is not

      rateLimiter.clearStale();
      expect(rateLimiter.cacheSize()).toBe(1);

      // key2 should still be in the cache, and thus rate-limited
      expect(rateLimiter.isAllowed("key2")).toBe(false);
      // key1 should have been removed, so it's allowed again
      expect(rateLimiter.isAllowed("key1")).toBe(true);
      expect(rateLimiter.cacheSize()).toBe(2);
    });
  });

  it("should periodically clean up expired keys", () => {
    const mathRandomSpy = vi.spyOn(Math, "random").mockReturnValue(0.5);
    const rateLimiter = newRateLimiter(windowSizeMs);

    // Add key1, cache size is 1.
    rateLimiter.isAllowed("key1");
    expect(rateLimiter.cacheSize()).toBe(1);

    // Advance time so key1 becomes stale.
    vi.advanceTimersByTime(windowSizeMs + 1);

    // Trigger another call for a different key.
    // This should not clear anything, cache size becomes 2.
    rateLimiter.isAllowed("key2");
    expect(rateLimiter.cacheSize()).toBe(2);

    // Mock random to trigger clearStale on the next call.
    mathRandomSpy.mockReturnValue(0.005);

    // This call for a new key ("key3") should trigger a cleanup.
    // "key1" is stale and will be cleared. "key2" remains. "key3" is added.
    // Cache size should go from 2 -> 1 (clear) -> 2 (add).
    rateLimiter.isAllowed("key3");
    expect(rateLimiter.cacheSize()).toBe(2);

    // To confirm "key1" was cleared, we should be able to add it again.
    expect(rateLimiter.isAllowed("key1")).toBe(true);
    expect(rateLimiter.cacheSize()).toBe(3);

    mathRandomSpy.mockRestore();
  });
});

```

--------------------------------------------------------------------------------
/packages/cli/commands/rules.ts:
--------------------------------------------------------------------------------

```typescript
import { confirm } from "@inquirer/prompts";
import chalk from "chalk";
import { Command } from "commander";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, relative } from "node:path";
import ora from "ora";

import { getCopilotInstructions, getCursorRules } from "../services/rules.js";
import { configStore } from "../stores/config.js";
import { handleError } from "../utils/errors.js";
import { fileExists } from "../utils/file.js";
import { rulesFormatOption, yesOption } from "../utils/options.js";

type RulesArgs = {
  format?: string;
  yes?: boolean;
};

const REFLAG_SECTION_START = "<!-- REFLAG_START -->";
const REFLAG_SECTION_END = "<!-- REFLAG_END -->";

async function confirmOverwrite(
  filePath: string,
  yes: boolean,
  append: boolean = false,
): Promise<boolean> {
  if (yes) return true;

  if (await fileExists(filePath)) {
    const projectPath = configStore.getProjectPath();
    const relativePath = relative(projectPath, filePath);

    return await confirm({
      message: `Rules ${chalk.cyan(relativePath)} already exists. ${
        append ? "Append rules?" : "Overwrite rules?"
      }`,
      default: false,
    });
  }

  return true;
}

function wrapInMarkers(content: string): string {
  return `${REFLAG_SECTION_START}\n\n${content}\n\n${REFLAG_SECTION_END}`;
}

function replaceOrAppendSection(
  existingContent: string,
  newContent: string,
): string {
  const wrappedContent = wrapInMarkers(newContent);
  const sectionRegex = new RegExp(
    `${REFLAG_SECTION_START}[\\s\\S]*?${REFLAG_SECTION_END}`,
    "g",
  );

  if (sectionRegex.test(existingContent)) {
    return existingContent.replace(sectionRegex, wrappedContent);
  }

  return `${existingContent}\n\n${wrappedContent}`;
}

export const rulesAction = async ({
  format = "cursor",
  yes = false,
}: RulesArgs = {}) => {
  const projectPath = configStore.getProjectPath();
  const appendFormats = ["copilot"];
  let destPath: string;
  let content: string;

  // Determine destination and content based on format
  if (format === "cursor") {
    destPath = join(projectPath, ".cursor", "rules", "reflag.mdc");
    content = getCursorRules();
  } else if (format === "copilot") {
    destPath = join(projectPath, ".github", "copilot-instructions.md");
    content = getCopilotInstructions();
  } else {
    console.error(`No rules added. Invalid format ${chalk.cyan(format)}.`);
    return;
  }

  // Check for overwrite and write file
  if (await confirmOverwrite(destPath, yes, appendFormats.includes(format))) {
    const spinner = ora("Adding rules...").start();
    try {
      await mkdir(dirname(destPath), { recursive: true });

      if (appendFormats.includes(format) && (await fileExists(destPath))) {
        const existingContent = await readFile(destPath, "utf-8");
        content = replaceOrAppendSection(existingContent, content);
      }

      await writeFile(destPath, content);
      spinner.succeed(
        `Rules added to ${chalk.cyan(relative(projectPath, destPath))}.
${chalk.grey("These rules should be committed to your project's version control.")}`,
      );
    } catch (error) {
      spinner.fail("Failed to add rules.");
      handleError(error, "Rules");
    }
  } else {
    console.log("Skipping adding rules.");
  }
};

export function registerRulesCommand(cli: Command) {
  cli
    .command("rules")
    .description("Add Reflag LLM rules to your project.")
    .addOption(rulesFormatOption)
    .addOption(yesOption)
    .action(rulesAction);
}

```

--------------------------------------------------------------------------------
/packages/vue-sdk/src/types.ts:
--------------------------------------------------------------------------------

```typescript
import type { Ref } from "vue";

import type {
  CompanyContext,
  InitOptions,
  RawFlags,
  ReflagClient,
  ReflagContext,
  RequestFeedbackData,
  UserContext,
} from "@reflag/browser-sdk";

export type EmptyFlagRemoteConfig = { key: undefined; payload: undefined };

export type FlagType = {
  config?: {
    payload: any;
  };
};

export type FlagRemoteConfig =
  | {
      key: string;
      payload: any;
    }
  | EmptyFlagRemoteConfig;

export interface Flag<
  TConfig extends FlagType["config"] = EmptyFlagRemoteConfig,
> {
  key: string;
  isEnabled: Ref<boolean>;
  isLoading: Ref<boolean>;
  config: Ref<({ key: string } & TConfig) | EmptyFlagRemoteConfig>;
  track(): Promise<Response | undefined> | undefined;
  requestFeedback: (opts: RequestFlagFeedbackOptions) => void;
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface Flags {}

export type TypedFlags = keyof Flags extends never
  ? Record<string, Flag>
  : {
      [TypedFlagKey in keyof Flags]: Flags[TypedFlagKey] extends FlagType
        ? Flag<Flags[TypedFlagKey]["config"]>
        : Flag;
    };

export type FlagKey = keyof TypedFlags;

export interface ProviderContextType {
  client: ReflagClient;
  isLoading: Ref<boolean>;
}

export type BootstrappedFlags = {
  context: ReflagContext;
  flags: RawFlags;
};

export type RequestFlagFeedbackOptions = Omit<
  RequestFeedbackData,
  "flagKey" | "featureId"
>;

/**
 * Base init options for the ReflagProvider and ReflagBootstrappedProvider.
 * @internal
 */
export type ReflagInitOptionsBase = Omit<
  InitOptions,
  "user" | "company" | "other" | "otherContext" | "bootstrappedFlags"
>;

/**
 * Base props for the ReflagProvider and ReflagBootstrappedProvider.
 * @internal
 */
export type ReflagBaseProps = {
  /**
   * Set to `true` to show the loading component while the client is initializing.
   */
  initialLoading?: boolean;

  /**
   * Set to `true` to enable debug logging to the console.
   */
  debug?: boolean;
};

/**
 * Props for the ReflagClientProvider.
 */
export type ReflagClientProviderProps = Omit<ReflagBaseProps, "debug"> & {
  /**
   * A pre-initialized ReflagClient to use.
   */
  client: ReflagClient;
};

/**
 * Props for the ReflagProvider.
 */
export type ReflagProps = ReflagInitOptionsBase &
  ReflagBaseProps & {
    /**
     * The context to use for the ReflagClient containing user, company, and other context.
     */
    context?: ReflagContext;

    /**
     * Company related context. If you provide `id` Reflag will enrich the evaluation context with
     * company attributes on Reflag servers.
     * @deprecated Use `context` instead, this property will be removed in the next major version
     */
    company?: CompanyContext;

    /**
     * User related context. If you provide `id` Reflag will enrich the evaluation context with
     * user attributes on Reflag servers.
     * @deprecated Use `context` instead, this property will be removed in the next major version
     */
    user?: UserContext;

    /**
     * Context which is not related to a user or a company.
     * @deprecated Use `context` instead, this property will be removed in the next major version
     */
    otherContext?: Record<string, string | number | undefined>;
  };

/**
 * Props for the ReflagBootstrappedProvider.
 */
export type ReflagBootstrappedProps = ReflagInitOptionsBase &
  ReflagBaseProps & {
    /**
     * Pre-fetched flags to be used instead of fetching them from the server.
     */
    flags: BootstrappedFlags;
  };

```

--------------------------------------------------------------------------------
/packages/node-sdk/test/inRequestCache.test.ts:
--------------------------------------------------------------------------------

```typescript
import {
  afterAll,
  afterEach,
  beforeAll,
  beforeEach,
  describe,
  expect,
  it,
  vi,
} from "vitest";

import cache from "../src/inRequestCache";
import { Logger } from "../src/types";

describe("inRequestCache", () => {
  let fn: () => Promise<number>;
  let logger: Logger;

  beforeAll(() => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
  });

  afterAll(() => {
    vi.useRealTimers();
  });

  beforeEach(() => {
    fn = vi.fn().mockResolvedValue(42);
    logger = {
      debug: vi.fn(),
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    };
  });

  it("should update the cached value when refreshing", async () => {
    const cached = cache(1000, logger, fn);

    const result = await cached.refresh();

    expect(result).toBe(42);
    expect(logger.debug).toHaveBeenCalledWith(
      expect.stringMatching("inRequestCache: fetched value"),
      42,
    );
  });

  it("should not allow multiple refreses at the same time", async () => {
    const cached = cache(1000, logger, fn);

    void cached.refresh();
    void cached.refresh();
    void cached.refresh();
    await cached.refresh();

    expect(fn).toHaveBeenCalledTimes(1);
    expect(logger.debug).toHaveBeenNthCalledWith(
      1,
      expect.stringMatching("inRequestCache: fetched value"),
      42,
    );

    void cached.refresh();
    await cached.refresh();

    expect(fn).toHaveBeenCalledTimes(2);
    expect(logger.debug).toHaveBeenNthCalledWith(
      2,
      expect.stringMatching("inRequestCache: fetched value"),
      42,
    );
  });

  it("should warn if the cached value is stale", async () => {
    const cached = cache(1000, logger, fn);

    await cached.refresh();

    vi.advanceTimersByTime(1100);

    const result = cached.get();

    expect(result).toBe(42);
    expect(logger.debug).toHaveBeenCalledWith(
      expect.stringMatching(
        "inRequestCache: stale value, triggering background refresh",
      ),
    );
  });

  it("should handle update failures gracefully", async () => {
    const error = new Error("update failed");
    fn = vi.fn().mockRejectedValueOnce(error).mockResolvedValueOnce(42);

    const cached = cache(1000, logger, fn);

    const first = await cached.refresh();

    expect(first).toBeUndefined();
    expect(logger.error).toHaveBeenCalledWith(
      expect.stringMatching("inRequestCache: error refreshing value"),
      error,
    );
    expect(fn).toHaveBeenCalledTimes(1);

    await cached.refresh();

    expect(fn).toHaveBeenCalledTimes(2);
    expect(logger.debug).toHaveBeenCalledWith(
      expect.stringMatching("inRequestCache: fetched value"),
      42,
    );

    const second = cached.get();
    expect(second).toBe(42);
  });

  it("should retain the cached value if the new value is undefined", async () => {
    fn = vi.fn().mockResolvedValueOnce(42).mockResolvedValueOnce(undefined);
    const cached = cache(1000, logger, fn);

    await cached.refresh();

    const second = cached.get();
    expect(second).toBe(42);

    // error refreshing
    await cached.refresh();

    // should still be the old value
    const result = cached.get();

    expect(result).toBe(42);
  });

  it("should not update if cached value is still valid", async () => {
    const cached = cache(1000, logger, fn);

    const first = await cached.refresh();

    vi.advanceTimersByTime(500);

    const second = cached.get();

    expect(first).toBe(second);
    expect(logger.debug).toHaveBeenCalledTimes(1); // Only one update call
  });

  afterEach(() => {
    vi.clearAllTimers();
    vi.restoreAllMocks();
  });
});

```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/flag/flagCache.ts:
--------------------------------------------------------------------------------

```typescript
import { RawFlags } from "./flags";

interface StorageItem {
  get(): string | null;
  set(value: string): void;
}

interface cacheEntry {
  expireAt: number;
  staleAt: number;
  flags: RawFlags;
}

// Parse and validate an API flags response
export function parseAPIFlagsResponse(flagsInput: any): RawFlags | undefined {
  if (!isObject(flagsInput)) {
    return;
  }

  const flags: RawFlags = {};
  for (const key in flagsInput) {
    const flag = flagsInput[key];

    if (
      typeof flag.isEnabled !== "boolean" ||
      flag.key !== key ||
      typeof flag.targetingVersion !== "number" ||
      (flag.config && typeof flag.config !== "object") ||
      (flag.missingContextFields &&
        !Array.isArray(flag.missingContextFields)) ||
      (flag.ruleEvaluationResults && !Array.isArray(flag.ruleEvaluationResults))
    ) {
      return;
    }

    flags[key] = {
      isEnabled: flag.isEnabled,
      targetingVersion: flag.targetingVersion,
      key,
      config: flag.config,
      missingContextFields: flag.missingContextFields,
      ruleEvaluationResults: flag.ruleEvaluationResults,
    };
  }

  return flags;
}

export interface CacheResult {
  flags: RawFlags;
  stale: boolean;
}

export class FlagCache {
  private storage: StorageItem;
  private readonly staleTimeMs: number;
  private readonly expireTimeMs: number;

  constructor({
    storage,
    staleTimeMs,
    expireTimeMs,
  }: {
    storage: StorageItem;
    staleTimeMs: number;
    expireTimeMs: number;
  }) {
    this.storage = storage;
    this.staleTimeMs = staleTimeMs;
    this.expireTimeMs = expireTimeMs;
  }

  set(
    key: string,
    {
      flags,
    }: {
      flags: RawFlags;
    },
  ) {
    let cacheData: CacheData = {};

    try {
      const cachedResponseRaw = this.storage.get();
      if (cachedResponseRaw) {
        cacheData = validateCacheData(JSON.parse(cachedResponseRaw)) ?? {};
      }
    } catch {
      // ignore errors
    }

    cacheData[key] = {
      expireAt: Date.now() + this.expireTimeMs,
      staleAt: Date.now() + this.staleTimeMs,
      flags,
    } satisfies cacheEntry;

    cacheData = Object.fromEntries(
      Object.entries(cacheData).filter(([_k, v]) => v.expireAt > Date.now()),
    );

    this.storage.set(JSON.stringify(cacheData));

    return cacheData;
  }

  get(key: string): CacheResult | undefined {
    try {
      const cachedResponseRaw = this.storage.get();
      if (cachedResponseRaw) {
        const cachedResponse = validateCacheData(JSON.parse(cachedResponseRaw));
        if (
          cachedResponse &&
          cachedResponse[key] &&
          cachedResponse[key].expireAt > Date.now()
        ) {
          return {
            flags: cachedResponse[key].flags,
            stale: cachedResponse[key].staleAt < Date.now(),
          };
        }
      }
    } catch {
      // ignore errors
    }
    return;
  }
}

type CacheData = Record<string, cacheEntry>;
function validateCacheData(cacheDataInput: any) {
  if (!isObject(cacheDataInput)) {
    return;
  }

  const cacheData: CacheData = {};
  for (const key in cacheDataInput) {
    const cacheEntry = cacheDataInput[key];
    if (!isObject(cacheEntry)) return;

    if (
      typeof cacheEntry.expireAt !== "number" ||
      typeof cacheEntry.staleAt !== "number" ||
      (cacheEntry.flags && !parseAPIFlagsResponse(cacheEntry.flags))
    ) {
      return;
    }

    cacheData[key] = {
      expireAt: cacheEntry.expireAt,
      staleAt: cacheEntry.staleAt,
      flags: cacheEntry.flags,
    };
  }
  return cacheData;
}

/**
 * Check if the given item is an object.
 *
 * @param item - The item to check.
 * @returns `true` if the item is an object, `false` otherwise.
 **/
export function isObject(item: any): item is Record<string, any> {
  return (item && typeof item === "object" && !Array.isArray(item)) || false;
}

```

--------------------------------------------------------------------------------
/packages/node-sdk/examples/express/app.ts:
--------------------------------------------------------------------------------

```typescript
import reflag from "./reflag";
import express from "express";
import { BoundReflagClient } from "../src";

// Augment the Express types to include the `reflagUser` property on the `res.locals` object
// This will allow us to access the ReflagClient instance in our route handlers
// without having to pass it around manually
declare global {
  namespace Express {
    interface Locals {
      reflagUser: BoundReflagClient;
    }
  }
}

const app = express();

app.use(express.json());
app.use((req, res, next) => {
  // Extract the user and company IDs from the request headers
  // You'll want to use a proper authentication and identification
  // mechanism in a real-world application
  const { user, company } = extractReflagContextFromHeader(req);

  // Create a new BoundReflagClient instance by calling the `bindClient` method on a `ReflagClient` instance
  // This will create a new instance that is bound to the user/company given.
  const reflagUser = reflag.bindClient({ user, company });

  // Store the BoundReflagClient instance in the `res.locals` object so we can access it in our route handlers
  res.locals.reflagUser = reflagUser;
  next();
});

export const todos = ["Buy milk", "Walk the dog"];

app.get("/", (_req, res) => {
  res.locals.reflagUser.track("Front Page Viewed");
  res.json({ message: "Ready to manage some TODOs!" });
});

// Return todos if the feature is enabled for the user
app.get("/todos", async (_req, res) => {
  // We use the `getFlag` method to check if the user has the "show-todos" feature enabled.
  // Note that "show-todos" is a feature that we defined in the `Flags` interface in the `reflag.ts` file.
  // and that the indexing for feature name below is type-checked at compile time.
  const { isEnabled, track } = res.locals.reflagUser.getFlag("show-todos");

  if (isEnabled) {
    track();

    // You can instead also send any custom event if you prefer, including attributes.
    // res.locals.reflagUser.track("Todo's viewed", { attributes: { access: "api" } });

    return res.json({ todos });
  }

  // Return no todos if the feature is disabled for the user
  return res.json({ todos: [] });
});

app.post("/todos", (req, res) => {
  const { todo } = req.body;

  if (typeof todo !== "string") {
    return res.status(400).json({ error: "Invalid todo" });
  }

  const { track, isEnabled, config } =
    res.locals.reflagUser.getFlag("create-todos");

  // Check if the user has the "create-todos" feature enabled
  if (isEnabled) {
    // Check if the todo is at least N characters long
    if (todo.length < config.payload.minimumLength) {
      return res
        .status(400)
        .json({ error: "Todo must be at least 5 characters long" });
    }

    // Track the feature usage
    track();
    todos.push(todo);

    return res.status(201).json({ todo });
  }

  res
    .status(403)
    .json({ error: "You do not have access to this feature yet!" });
});

app.delete("/todos/:idx", (req, res) => {
  const idx = parseInt(req.params.idx);

  if (isNaN(idx) || idx < 0 || idx >= todos.length) {
    return res.status(400).json({ error: "Invalid index" });
  }

  const { track, isEnabled } = res.locals.reflagUser.getFlag("delete-todos");

  if (isEnabled) {
    todos.splice(idx, 1);

    track();
    res.json({});
  }

  res
    .status(403)
    .json({ error: "You do not have access to this feature yet!" });
});

app.get("/features", async (_req, res) => {
  const features = await res.locals.reflagUser.getFlagsRemote();
  res.json(features);
});

export default app;

function extractReflagContextFromHeader(req: express.Request) {
  const user = req.headers["x-reflag-user-id"]
    ? {
        id: req.headers["x-reflag-user-id"] as string,
        role: req.headers["x-reflag-is-admin"] ? "admin" : "user",
      }
    : undefined;
  const company = req.headers["x-reflag-company-id"]
    ? {
        id: req.headers["x-reflag-company-id"] as string,
        betaUser: !!req.headers["x-reflag-company-beta-user"],
      }
    : undefined;
  return { user, company };
}

```

--------------------------------------------------------------------------------
/packages/cli/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import chalk from "chalk";
import { program } from "commander";
import ora from "ora";

import { registerAppCommands } from "./commands/apps.js";
import { registerAuthCommands } from "./commands/auth.js";
import { registerFlagCommands } from "./commands/flags.js";
import { registerInitCommand } from "./commands/init.js";
import { registerMcpCommand } from "./commands/mcp.js";
import { registerNewCommand } from "./commands/new.js";
import { registerRulesCommand } from "./commands/rules.js";
import { bootstrap, getReflagUser } from "./services/bootstrap.js";
import { authStore } from "./stores/auth.js";
import { configStore } from "./stores/config.js";
import { commandName } from "./utils/commander.js";
import { handleError } from "./utils/errors.js";
import {
  apiKeyOption,
  apiUrlOption,
  baseUrlOption,
  debugOption,
} from "./utils/options.js";
import { stripTrailingSlash } from "./utils/urls.js";
import { checkLatest as checkLatestVersion } from "./utils/version.js";

const skipBootstrapCommands = [/^login/, /^logout/, /^rules/];

type Options = {
  debug?: boolean;
  baseUrl?: string;
  apiUrl?: string;
  apiKey?: string;
};

async function main() {
  // Start a version check in the background
  // unhandled promise rejection can happen even without the `await`
  // so we need a `catch` here.
  const cliVersionCheckPromise = checkLatestVersion().catch(() => ({
    latestVersion: "unknown",
    currentVersion: "unknown",
    isNewerAvailable: false,
  }));

  // Must load tokens and config before anything else
  await authStore.initialize();
  await configStore.initialize();

  // Global options
  program.addOption(debugOption);
  program.addOption(baseUrlOption);
  program.addOption(apiUrlOption);
  program.addOption(apiKeyOption);

  // Pre-action hook
  program.hook("preAction", async (_, actionCommand) => {
    const {
      debug,
      baseUrl,
      apiUrl,
      apiKey: explicitApiKey,
    } = program.opts<Options>();
    const cleanedBaseUrl = stripTrailingSlash(baseUrl?.trim());
    const cleanedApiUrl = stripTrailingSlash(apiUrl?.trim());

    const apiKey = explicitApiKey ?? process.env.REFLAG_API_KEY;

    if (typeof apiKey === "string" && apiKey.length > 0) {
      console.info(
        chalk.yellow(
          "API key supplied. Using it instead of normal personal authentication.",
        ),
      );
      authStore.useApiKey(apiKey);
    }

    // Set baseUrl and apiUrl in config store, will skip if undefined
    configStore.setConfig({
      baseUrl: cleanedBaseUrl,
      apiUrl: cleanedApiUrl || (cleanedBaseUrl && `${cleanedBaseUrl}/api`),
    });

    // Skip bootstrapping for commands that don't require it
    if (
      !skipBootstrapCommands.some((cmd) => cmd.test(commandName(actionCommand)))
    ) {
      const spinner = ora("Bootstrapping...").start();

      try {
        // Load bootstrap data if not already loaded
        await bootstrap();
        spinner.stop();
      } catch (error) {
        spinner.fail("Bootstrap failed.");
        handleError(error, "Connect");
      }
    }

    const { latestVersion, currentVersion, isNewerAvailable } =
      await cliVersionCheckPromise;

    if (isNewerAvailable) {
      console.info(
        `A new version of the CLI is available: ${chalk.yellow(
          currentVersion,
        )} -> ${chalk.green(latestVersion)}. Update to ensure you have the latest features and bug fixes.`,
      );
    }

    if (debug) {
      console.debug(chalk.cyan("\nDebug mode enabled."));
      const user = getReflagUser();
      console.debug(`Logged in as ${chalk.cyan(user.name ?? user.email)}.`);
      console.debug(
        "Reading config from:",
        chalk.cyan(configStore.getConfigPath()),
      );
      console.table(configStore.getConfig());
    }
  });

  // Main program
  registerNewCommand(program);
  registerInitCommand(program);
  registerAuthCommands(program);
  registerAppCommands(program);
  registerFlagCommands(program);
  registerMcpCommand(program);
  registerRulesCommand(program);

  program.parse(process.argv);
}

void main();

```

--------------------------------------------------------------------------------
/packages/browser-sdk/test/hooksManager.test.ts:
--------------------------------------------------------------------------------

```typescript
import { beforeEach, describe, expect, it, vi } from "vitest";

import { CompanyContext, UserContext } from "../src";
import { CheckEvent, RawFlags } from "../src/flag/flags";
import { HooksManager } from "../src/hooksManager";

describe("HookManager", () => {
  let hookManager: HooksManager;

  beforeEach(() => {
    hookManager = new HooksManager();
  });

  it("should add and trigger `check` hooks (is-enabled)", () => {
    const callback = vi.fn();
    hookManager.addHook("check", callback);

    const checkEvent: CheckEvent = {
      action: "check-is-enabled",
      key: "test-key",
      value: true,
    };
    hookManager.trigger("check", checkEvent);

    expect(callback).toHaveBeenCalledWith(checkEvent);
  });

  it("should add and trigger `check` hooks (config)", () => {
    const callback = vi.fn();
    hookManager.addHook("check", callback);

    const checkEvent: CheckEvent = {
      action: "check-config",
      key: "test-key",
      value: { key: "key", payload: "payload" },
    };
    hookManager.trigger("check", checkEvent);

    expect(callback).toHaveBeenCalledWith(checkEvent);
  });

  it("should add and trigger `flagsUpdated` hooks", () => {
    const callback = vi.fn();
    hookManager.addHook("flagsUpdated", callback);

    const flags: RawFlags = {
      /* mock RawFlags data */
    };
    hookManager.trigger("flagsUpdated", flags);

    expect(callback).toHaveBeenCalledWith(flags);
  });

  it("should add and trigger `track` hooks", () => {
    const callback = vi.fn();
    const user: UserContext = { id: "user-id", name: "user-name" };
    const company: CompanyContext = { id: "company-id", name: "company-name" };
    hookManager.addHook("track", callback);

    const eventName = "test-event";
    const attributes = { key: "value" };
    hookManager.trigger("track", { eventName, attributes, user, company });

    expect(callback).toHaveBeenCalledWith({
      eventName,
      attributes,
      user,
      company,
    });
  });

  it("should add and trigger `user` hooks", () => {
    const callback = vi.fn();

    hookManager.addHook("user", callback);

    const user = { id: "user-id", name: "user-name" };
    hookManager.trigger("user", user);

    expect(callback).toHaveBeenCalledWith(user);
  });

  it("should add and trigger `company` hooks", () => {
    const callback = vi.fn();
    hookManager.addHook("company", callback);

    const company = { id: "company-id", name: "company-name" };
    hookManager.trigger("company", company);

    expect(callback).toHaveBeenCalledWith(company);
  });

  it("should handle multiple hooks of the same type", () => {
    const callback1 = vi.fn();
    const callback2 = vi.fn();

    hookManager.addHook("check", callback1);
    hookManager.addHook("check", callback2);

    const checkEvent: CheckEvent = {
      action: "check-is-enabled",
      key: "test-key",
      value: true,
    };
    hookManager.trigger("check", checkEvent);

    expect(callback1).toHaveBeenCalledWith(checkEvent);
    expect(callback2).toHaveBeenCalledWith(checkEvent);
  });

  it("should remove the given hook and no other hooks", () => {
    const callback1 = vi.fn();
    const callback2 = vi.fn();

    hookManager.addHook("check", callback1);
    hookManager.addHook("check", callback2);
    hookManager.removeHook("check", callback1);

    const checkEvent: CheckEvent = {
      action: "check-is-enabled",
      key: "test-key",
      value: true,
    };
    hookManager.trigger("check", checkEvent);

    expect(callback1).not.toHaveBeenCalled();
    expect(callback2).toHaveBeenCalledWith(checkEvent);
  });

  it("should remove the hook using the function returned from addHook", () => {
    const callback1 = vi.fn();
    const callback2 = vi.fn();

    const removeHook1 = hookManager.addHook("check", callback1);
    hookManager.addHook("check", callback2);
    removeHook1();

    const checkEvent: CheckEvent = {
      action: "check-is-enabled",
      key: "test-key",
      value: true,
    };
    hookManager.trigger("check", checkEvent);

    expect(callback1).not.toHaveBeenCalled();
    expect(callback2).toHaveBeenCalledWith(checkEvent);
  });
});

```

--------------------------------------------------------------------------------
/packages/node-sdk/test/fetch-http-client.test.ts:
--------------------------------------------------------------------------------

```typescript
import { afterEach, describe, expect, it, vi } from "vitest";

import { API_TIMEOUT_MS } from "../src/config";
import fetchClient from "../src/fetch-http-client";

// mock environment variables
vi.mock("../src/config", () => ({ API_TIMEOUT_MS: 100 }));

describe("fetchClient", () => {
  const url = "https://example.com/api";
  const headers = { "Content-Type": "application/json" };

  afterEach(() => {
    vi.resetAllMocks();
  });

  it("should make a POST request and return the response", async () => {
    const body = { key: "value" };
    const response = { ok: true, status: 200, body: { success: true } };

    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: async () => ({
        success: true,
      }),
    } as Response);

    const result = await fetchClient.post<typeof body, typeof response>(
      url,
      headers,
      body,
    );

    expect(result).toEqual(response);
    expect(global.fetch).toHaveBeenCalledTimes(1);
    expect(global.fetch).toHaveBeenCalledWith(
      url,
      expect.objectContaining({
        method: "post",
        headers,
        body: JSON.stringify(body),
        signal: expect.any(AbortSignal),
      }),
    );
  });

  it("should make a GET request and return the response", async () => {
    const response = { ok: true, status: 200, body: { success: true } };

    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: async () => ({
        success: true,
      }),
    } as Response);

    const result = await fetchClient.get<typeof response>(url, headers);

    expect(result).toEqual(response);
    expect(global.fetch).toHaveBeenCalledTimes(1);
    expect(global.fetch).toHaveBeenCalledWith(
      url,
      expect.objectContaining({
        method: "get",
        headers,
        signal: expect.any(AbortSignal),
      }),
    );
  });

  it("should timeout a POST request that takes too long", async () => {
    global.fetch = vi
      .fn()
      .mockImplementation(
        () =>
          new Promise((resolve) =>
            setTimeout(
              () => resolve({ ok: true, json: async () => ({}) }),
              API_TIMEOUT_MS + 100,
            ),
          ),
      );

    await fetchClient.post(url, headers, {});
    expect(vi.mocked(global.fetch).mock.calls[0][1]?.signal?.aborted).toBe(
      true,
    );
  });

  it("should timeout a GET request that takes too long", async () => {
    global.fetch = vi
      .fn()
      .mockImplementation(
        () =>
          new Promise((resolve) =>
            setTimeout(
              () => resolve({ ok: true, json: async () => ({}) }),
              API_TIMEOUT_MS + 100,
            ),
          ),
      );

    await fetchClient.get(url, headers);
    expect(vi.mocked(global.fetch).mock.calls[0][1]?.signal?.aborted).toBe(
      true,
    );
  });

  it("should handle POST non-20x responses", async () => {
    const response = {
      ok: false,
      status: 400,
      body: { error: "Something went wrong" },
    };

    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 400,
      json: async () => ({ error: "Something went wrong" }),
    } as Response);

    const result = await fetchClient.post(url, headers, {});

    expect(result).toEqual(response);
  });

  it("should handle GET non-20x responses", async () => {
    const response = {
      ok: false,
      status: 400,
      body: { error: "Something went wrong" },
    };

    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 400,
      json: async () => ({ error: "Something went wrong" }),
    } as Response);

    const result = await fetchClient.get(url, headers);

    expect(result).toEqual(response);
  });

  it("should not handle POST exceptions", async () => {
    global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));

    await expect(fetchClient.post(url, headers, {})).rejects.toThrow(
      "Network error",
    );
  });

  it("should not handle GET exceptions", async () => {
    global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));

    await expect(fetchClient.get(url, headers)).rejects.toThrow(
      "Network error",
    );
  });
});

```

--------------------------------------------------------------------------------
/packages/cli/utils/gen.ts:
--------------------------------------------------------------------------------

```typescript
import { camelCase, kebabCase, pascalCase, snakeCase } from "change-case";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, isAbsolute, join } from "node:path";

import { Flag, RemoteConfig } from "../services/flags.js";

import { JSONToType, quoteKey } from "./json.js";

export type GenFormat = "react" | "node";

// Keep in sync with Reflag main repo
export const KeyFormats = [
  "custom",
  "pascalCase",
  "camelCase",
  "snakeCaseUpper",
  "snakeCaseLower",
  "kebabCaseUpper",
  "kebabCaseLower",
] as const;

export type KeyFormat = (typeof KeyFormats)[number];

type KeyFormatPattern = {
  transform: (key: string) => string;
  regex: RegExp;
  message: string;
};

export const KeyFormatPatterns: Record<KeyFormat, KeyFormatPattern> = {
  custom: {
    transform: (key) => key?.trim(),
    regex: /^[\p{L}\p{N}\p{P}\p{S}\p{Z}]+$/u,
    message:
      "Key must contain only letters, numbers, punctuation, symbols, or spaces.",
  },
  pascalCase: {
    transform: (key) => pascalCase(key),
    regex: /^[\p{Lu}][\p{L}\p{N}]*$/u,
    message:
      "Key must start with uppercase letter and contain only letters and numbers.",
  },
  camelCase: {
    transform: (key) => camelCase(key),
    regex: /^[\p{Ll}][\p{L}\p{N}]*$/u,
    message:
      "Key must start with lowercase letter and contain only letters and numbers.",
  },
  snakeCaseUpper: {
    transform: (key) => snakeCase(key).toUpperCase(),
    regex: /^[\p{Lu}][\p{Lu}\p{N}]*(?:_[\p{Lu}\p{N}]+)*$/u,
    message: "Key must be uppercase with words separated by underscores.",
  },
  snakeCaseLower: {
    transform: (key) => snakeCase(key).toLowerCase(),
    regex: /^[\p{Ll}][\p{Ll}\p{N}]*(?:_[\p{Ll}\p{N}]+)*$/u,
    message: "Key must be lowercase with words separated by underscores.",
  },
  kebabCaseUpper: {
    transform: (key) => kebabCase(key).toUpperCase(),
    regex: /^[\p{Lu}][\p{Lu}\p{N}]*(?:-[\p{Lu}\p{N}]+)*$/u,
    message: "Key must be uppercase with words separated by hyphens.",
  },
  kebabCaseLower: {
    transform: (key) => kebabCase(key).toLowerCase(),
    regex: /^[\p{Ll}][\p{Ll}\p{N}]*(?:-[\p{Ll}\p{N}]+)*$/u,
    message: "Key must be lowercase with words separated by hyphens.",
  },
};

export function indentLines(
  str: string,
  indent = 2,
  lineBreak = "\n",
  trim = false,
): string {
  const indentStr = " ".repeat(indent);
  return str
    .split(lineBreak)
    .map((line) => `${indentStr}${trim ? line.trim() : line}`)
    .join(lineBreak);
}

export function genFlagKey(input: string, format: KeyFormat): string {
  return KeyFormatPatterns[format].transform(input);
}

export function genRemoteConfig(remoteConfigs?: RemoteConfig[]) {
  const variants = remoteConfigs?.[0]?.variants;
  if (!variants?.length) return;
  return JSONToType(
    remoteConfigs![0].variants?.map(({ variant: { payload } }) => payload),
  );
}

export function genTypes(flags: Flag[], format: GenFormat = "react") {
  const configDefs = new Map<string, { name: string; definition: string }>();
  flags.forEach(({ key, name, remoteConfigs }) => {
    const definition = genRemoteConfig(remoteConfigs);

    if (!definition) {
      return;
    }

    const configName = `${pascalCase(name)}ConfigPayload`;
    configDefs.set(key, { name: configName, definition });
  });

  return /* ts */ `
// DO NOT EDIT THIS FILE. IT IS GENERATED BY THE REFLAG CLI AND WILL BE OVERWRITTEN.
// eslint-disable
// prettier-ignore
import "@reflag/${format}-sdk";

declare module "@reflag/${format}-sdk" {
  export interface Flags {
${flags
  .map(({ key }) => {
    const config = configDefs.get(key);
    return indentLines(
      `${quoteKey(key)}: ${config?.definition ? `{ config: { payload: ${config.name} } }` : "boolean"};`,
      4,
    );
  })
  .join("\n")}
  }

${Array.from(configDefs.values())
  .map(({ name, definition }) => {
    return indentLines(`export type ${name} = ${definition}`);
  })
  .join("\n\n")}
}
`.trim();
}

export async function writeTypesToFile(
  types: string,
  outPath: string,
  projectPath: string,
) {
  const fullPath = isAbsolute(outPath) ? outPath : join(projectPath, outPath);

  await mkdir(dirname(fullPath), { recursive: true });
  await writeFile(fullPath, types);

  return fullPath;
}

```

--------------------------------------------------------------------------------
/packages/node-sdk/src/config.ts:
--------------------------------------------------------------------------------

```typescript
import { readFileSync } from "fs";

import { version } from "../package.json";

import { LOG_LEVELS } from "./types";
import { isObject, ok } from "./utils";

export const API_BASE_URL = "https://front.reflag.com";
export const SDK_VERSION_HEADER_NAME = "reflag-sdk-version";
export const SDK_VERSION = `node-sdk/${version}`;
export const API_TIMEOUT_MS = 10000;
export const END_FLUSH_TIMEOUT_MS = 5000;

export const REFLAG_LOG_PREFIX = "[Reflag]";

export const FLAG_EVENT_RATE_LIMITER_WINDOW_SIZE_MS = 60 * 1000;

export const FLAGS_REFETCH_MS = 60 * 1000; // re-fetch every 60 seconds

export const BATCH_MAX_SIZE = 100;
export const BATCH_INTERVAL_MS = 10 * 1000;

function parseOverrides(config: object | undefined) {
  if (!config) return {};
  if ("flagOverrides" in config && isObject(config.flagOverrides)) {
    Object.entries(config.flagOverrides).forEach(([key, value]) => {
      ok(
        typeof value === "boolean" || isObject(value),
        `invalid type "${typeof value}" for key ${key}, expected boolean or object`,
      );
      if (isObject(value)) {
        ok(
          "isEnabled" in value && typeof value.isEnabled === "boolean",
          `invalid type "${typeof value.isEnabled}" for key ${key}.isEnabled, expected boolean`,
        );
        ok(
          value.config === undefined || isObject(value.config),
          `invalid type "${typeof value.config}" for key ${key}.config, expected object or undefined`,
        );
        if (isObject(value.config)) {
          ok(
            "key" in value.config && typeof value.config.key === "string",
            `invalid type "${typeof value.config.key}" for key ${key}.config.key, expected string`,
          );
        }
      }
    });

    return config.flagOverrides;
  }

  return {};
}

function loadConfigFile(file: string) {
  const configJson = readFileSync(file, "utf-8");
  const config = JSON.parse(configJson);

  ok(typeof config === "object", "config must be an object");
  const { secretKey, logLevel, offline, host, apiBaseUrl } = config;

  ok(
    typeof secretKey === "undefined" || typeof secretKey === "string",
    "secret must be a string",
  );
  ok(
    typeof apiBaseUrl === "undefined" || typeof apiBaseUrl === "string",
    "apiBaseUrl must be a string",
  );
  ok(
    typeof logLevel === "undefined" ||
      (typeof logLevel === "string" && LOG_LEVELS.includes(logLevel as any)),
    `logLevel must one of ${LOG_LEVELS.join(", ")}`,
  );
  ok(
    typeof offline === "undefined" || typeof offline === "boolean",
    "offline must be a boolean",
  );

  return {
    flagOverrides: parseOverrides(config),
    secretKey,
    logLevel,
    offline,
    apiBaseUrl: host ?? apiBaseUrl,
  };
}

function loadEnvVars() {
  const secretKey = process.env.REFLAG_SECRET_KEY;
  const enabledFlags = process.env.REFLAG_FLAGS_ENABLED;
  const disabledFlags = process.env.REFLAG_FLAGS_DISABLED;
  const logLevel = process.env.REFLAG_LOG_LEVEL;
  const apiBaseUrl = process.env.REFLAG_API_BASE_URL;
  const offline =
    process.env.REFLAG_OFFLINE !== undefined
      ? ["true", "on"].includes(process.env.REFLAG_OFFLINE)
      : undefined;

  let flagOverrides: Record<string, boolean> = {};
  if (enabledFlags) {
    flagOverrides = enabledFlags.split(",").reduce(
      (acc, f) => {
        const key = f.trim();
        if (key) acc[key] = true;
        return acc;
      },
      {} as Record<string, boolean>,
    );
  }

  if (disabledFlags) {
    flagOverrides = {
      ...flagOverrides,
      ...disabledFlags.split(",").reduce(
        (acc, f) => {
          const key = f.trim();
          if (key) acc[key] = false;
          return acc;
        },
        {} as Record<string, boolean>,
      ),
    };
  }

  return { secretKey, flagOverrides, logLevel, offline, apiBaseUrl };
}

export function loadConfig(file?: string) {
  let fileConfig;
  if (file) {
    fileConfig = loadConfigFile(file);
  }

  const envConfig = loadEnvVars();

  return {
    secretKey: envConfig.secretKey || fileConfig?.secretKey,
    logLevel: envConfig.logLevel || fileConfig?.logLevel,
    offline: envConfig.offline ?? fileConfig?.offline,
    apiBaseUrl: envConfig.apiBaseUrl ?? fileConfig?.apiBaseUrl,
    flagOverrides: {
      ...fileConfig?.flagOverrides,
      ...envConfig.flagOverrides,
    },
  };
}

```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/toolbar/Flags.tsx:
--------------------------------------------------------------------------------

```typescript
import { Fragment, h } from "preact";

import { Switch } from "./Switch";
import { FlagItem } from "./Toolbar";

const isFound = (flagKey: string, searchQuery: string | null) => {
  return flagKey.toLocaleLowerCase().includes(searchQuery ?? "");
};

export function FlagsTable({
  flags,
  searchQuery,
  appBaseUrl,
  setIsEnabledOverride,
}: {
  flags: FlagItem[];
  searchQuery: string | null;
  appBaseUrl: string;
  setIsEnabledOverride: (key: string, isEnabled: boolean | null) => void;
}) {
  const hasFlags = flags.length > 0;
  const hasShownFlags = flags.some((flag) =>
    isFound(flag.flagKey, searchQuery),
  );

  // List flags that match the search query first then alphabetically
  const searchedFlags =
    searchQuery === null
      ? flags
      : [...flags].sort((a, b) => {
          const aMatches = isFound(a.flagKey, searchQuery);
          const bMatches = isFound(b.flagKey, searchQuery);

          // If both match or both don't match, sort alphabetically
          if (aMatches === bMatches) {
            const aStartsWith = a.flagKey
              .toLocaleLowerCase()
              .startsWith(searchQuery);
            const bStartsWith = b.flagKey
              .toLocaleLowerCase()
              .startsWith(searchQuery);

            // If one starts with search query and the other doesn't, prioritize the one that starts with it
            if (aStartsWith && !bStartsWith) return -1;
            if (bStartsWith && !aStartsWith) return 1;

            // Otherwise sort alphabetically
            return a.flagKey.localeCompare(b.flagKey);
          }

          // Otherwise, matching flags come first
          return aMatches ? -1 : 1;
        });

  return (
    <Fragment>
      {(!hasFlags || !hasShownFlags) && (
        <div class="flags-table-empty">
          No flags {hasFlags ? `matching "${searchQuery}"` : "found"}
        </div>
      )}
      <table class="flags-table">
        <tbody>
          {searchedFlags.map((flag, index) => (
            <FlagRow
              key={flag.flagKey}
              appBaseUrl={appBaseUrl}
              flag={flag}
              index={index}
              isNotVisible={
                searchQuery !== null && !isFound(flag.flagKey, searchQuery)
              }
              setEnabledOverride={(override) =>
                setIsEnabledOverride(flag.flagKey, override)
              }
            />
          ))}
        </tbody>
      </table>
    </Fragment>
  );
}

function FlagRow({
  setEnabledOverride,
  appBaseUrl,
  flag,
  index,
  isNotVisible,
}: {
  flag: FlagItem;
  appBaseUrl: string;
  setEnabledOverride: (isEnabled: boolean | null) => void;
  index: number;
  isNotVisible: boolean;
}) {
  const isEnabledOverride = flag.isEnabledOverride !== null;
  return (
    <tr
      key={flag.flagKey}
      class={["flag-row", isNotVisible ? "not-visible" : undefined].join(" ")}
    >
      <td class="flag-name-cell">
        <a
          class="flag-link"
          href={`${appBaseUrl}/env-current/flags/by-key/${flag.flagKey}`}
          rel="noreferrer"
          tabIndex={index + 1}
          target="_blank"
        >
          {flag.flagKey}
        </a>
      </td>
      <td class="flag-reset-cell">
        {isEnabledOverride ? (
          <Reset setEnabledOverride={setEnabledOverride} tabIndex={index + 1} />
        ) : null}
      </td>
      <td class="flag-switch-cell">
        <Switch
          checked={flag.isEnabledOverride ?? flag.isEnabled}
          tabIndex={index + 1}
          onChange={(e) => {
            const isChecked = e.currentTarget.checked;
            setEnabledOverride(!isEnabledOverride ? isChecked : null);
          }}
        />
      </td>
    </tr>
  );
}

export function FlagSearch({ onSearch }: { onSearch: (val: string) => void }) {
  return (
    <input
      class="search-input"
      placeholder="Search flags"
      tabIndex={0}
      type="search"
      autoFocus
      onInput={(s) => onSearch(s.currentTarget.value)}
    />
  );
}

function Reset({
  setEnabledOverride,
  ...props
}: {
  setEnabledOverride: (isEnabled: boolean | null) => void;
} & h.JSX.HTMLAttributes<HTMLAnchorElement>) {
  return (
    <a
      class="reset"
      href=""
      onClick={(e) => {
        e.preventDefault();
        setEnabledOverride(null);
      }}
      {...props}
    >
      reset
    </a>
  );
}

```

--------------------------------------------------------------------------------
/packages/node-sdk/test/periodicallyUpdatingCache.test.ts:
--------------------------------------------------------------------------------

```typescript
import {
  afterAll,
  afterEach,
  beforeAll,
  beforeEach,
  describe,
  expect,
  it,
  vi,
} from "vitest";

import cache from "../src/periodicallyUpdatingCache";
import { Logger } from "../src/types";

describe("cache", () => {
  let fn: () => Promise<number>;
  let logger: Logger;

  beforeAll(() => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
  });

  afterAll(() => {
    vi.useRealTimers();
  });

  beforeEach(() => {
    fn = vi.fn().mockResolvedValue(42);
    logger = {
      debug: vi.fn(),
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    };
  });

  it("should update the cached value when refreshing", async () => {
    const cached = cache(1000, 2000, logger, fn);

    const result = await cached.refresh();

    expect(result).toBe(42);
    expect(logger.debug).toHaveBeenCalledWith(
      expect.stringMatching("updated cached value"),
      42,
    );
  });

  it("should not allow multiple refreses at the same time", async () => {
    const cached = cache(1000, 2000, logger, fn);

    void cached.refresh();
    void cached.refresh();
    void cached.refresh();
    await cached.refresh();

    expect(fn).toHaveBeenCalledTimes(1);
    expect(logger.debug).toHaveBeenNthCalledWith(
      1,
      expect.stringMatching("updated cached value"),
      42,
    );

    void cached.refresh();
    await cached.refresh();

    expect(fn).toHaveBeenCalledTimes(2);
    expect(logger.debug).toHaveBeenNthCalledWith(
      2,
      expect.stringMatching("updated cached value"),
      42,
    );
  });

  it("should warn if the cached value is stale", async () => {
    const cached = cache(1000, 2000, logger, fn);

    await cached.refresh();

    vi.advanceTimersByTime(2500);

    const result = cached.get();

    expect(result).toBe(42);
    expect(logger.warn).toHaveBeenCalledWith(
      expect.stringMatching("cached value is stale"),
      {
        age: expect.any(Number),
        cachedValue: 42,
      },
    );
  });

  it("should update the cached value after ttl", async () => {
    const newValue = 84;
    fn = vi.fn().mockResolvedValueOnce(42).mockResolvedValueOnce(newValue);

    const cached = cache(1000, 2000, logger, fn);

    const first = await cached.refresh();

    expect(first).toBe(42);
    expect(fn).toHaveBeenCalledTimes(1);

    await vi.advanceTimersToNextTimerAsync();

    const second = cached.get();

    expect(second).toBe(newValue);
    expect(fn).toHaveBeenCalledTimes(2);

    expect(logger.debug).toHaveBeenCalledWith(
      expect.stringMatching("updated cached value"),
      newValue,
    );
  });

  it("should handle update failures gracefully", async () => {
    const error = new Error("update failed");
    fn = vi.fn().mockRejectedValueOnce(error).mockResolvedValueOnce(42);

    const cached = cache(1000, 2000, logger, fn);

    const first = await cached.refresh();

    expect(first).toBeUndefined();
    expect(logger.error).toHaveBeenCalledWith(
      expect.stringMatching("failed to update cached value"),
      error,
    );
    expect(fn).toHaveBeenCalledTimes(1);

    await vi.advanceTimersToNextTimerAsync();

    expect(fn).toHaveBeenCalledTimes(2);
    expect(logger.debug).toHaveBeenCalledWith(
      expect.stringMatching("updated cached value"),
      42,
    );

    const second = cached.get();
    expect(second).toBe(42);
  });

  it("should retain the cached value if the new value is undefined", async () => {
    fn = vi.fn().mockResolvedValueOnce(42).mockResolvedValueOnce(undefined);
    const cached = cache(1000, 2000, logger, fn);

    await cached.refresh();

    await vi.advanceTimersToNextTimerAsync();

    const second = cached.get();
    expect(second).toBe(42);

    vi.advanceTimersByTime(2500);

    const result = cached.get();

    expect(result).toBe(42);
    expect(logger.warn).toHaveBeenCalledWith(
      expect.stringMatching("cached value is stale"),
      {
        age: expect.any(Number),
        cachedValue: 42,
      },
    );
  });

  it("should not update if cached value is still valid", async () => {
    const cached = cache(1000, 2000, logger, fn);

    const first = await cached.refresh();

    vi.advanceTimersByTime(500);

    const second = cached.get();

    expect(first).toBe(second);
    expect(logger.debug).toHaveBeenCalledTimes(1); // Only one update call
  });

  afterEach(() => {
    vi.clearAllTimers();
    vi.restoreAllMocks();
  });
});

```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/toolbar/Toolbar.css:
--------------------------------------------------------------------------------

```css
/* Animations */

@keyframes bounceInUp {
  from,
  60%,
  75%,
  90%,
  to {
    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
  }

  from {
    opacity: 0;
    transform: translate3d(0, 50px, 0) scaleY(2);
  }

  60% {
    opacity: 1;
    transform: translate3d(0, -6px, 0) scaleY(0.9);
  }

  75% {
    transform: translate3d(0, 3px, 0) scaleY(0.95);
  }

  90% {
    transform: translate3d(0, -2px, 0) scaleY(0.985);
  }

  to {
    transform: translate3d(0, 0, 0);
  }
}

@keyframes gelatine {
  from,
  to {
    transform: scale(1, 1);
  }
  25% {
    transform: scale(0.9, 1.1);
  }
  50% {
    transform: scale(1.1, 0.9);
  }
  75% {
    transform: scale(0.95, 1.05);
  }
}

/* Toolbar */

.toolbar {
  --brand300: #9cc4d3;
  --brand400: #77adc1;
  --gray500: #787c91;
  --gray600: #3c3d49;
  --gray700: #22232a;
  --gray800: #17181c;
  --gray900: #0e0e11;
  --gray950: #09090b;
  --black: #1e1f24;
  --white: white;

  --bg-color: var(--gray900);
  --bg-light-color: var(--gray700);
  --border-color: var(--gray700);
  --dimmed-color: var(--gray500);

  --logo-color: white;
  --text-color: white;
  --text-size: 13px;
  --text-small-size: 12px;
  font-family:
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    Roboto,
    Oxygen,
    Ubuntu,
    Cantarell,
    "Open Sans",
    "Helvetica Neue",
    sans-serif;
  font-size: var(--text-size);
}

:focus {
  outline: none;
}

.dialog {
  color: #ffffff;
  box-sizing: border-box;
  background: var(--bg-color);

  border: 0;
  box-shadow:
    0 10px 15px -3px rgba(0, 0, 0, 0.15),
    0 4px 6px -2px rgba(0, 0, 0, 0.1),
    0 -1px rgba(255, 255, 255, 0.1),
    0 0 0 1px var(--border-color);
  border-radius: 7px;
  z-index: 999999;
  min-width: 240px;
  padding: 0;

  --visible-flags: 15;
  max-height: min(
    calc(100vh - 36px - 35px),
    calc(45px + (var(--visible-flags) * 27px))
  );
  height: auto;

  &[open] {
    display: flex;
    flex-direction: column;
  }
}

.dialog-content {
  overflow-y: auto;
  max-height: 100%;
  flex-grow: 1;
  margin: 3px 3px 3px 0;

  &::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }
  &::-webkit-scrollbar-track {
    background: transparent;
  }
  &::-webkit-scrollbar-thumb {
    background-color: rgba(255, 255, 255, 0.2);
    border-radius: 999px;
    transition: background-color 0.1s ease;

    &:hover {
      background-color: rgba(255, 255, 255, 0.3);
    }
  }
  &::-webkit-scrollbar-button {
    display: none;
  }
}

.toolbar-toggle {
  width: 36px;
  height: 36px;
  position: fixed;
  z-index: 999999;
  padding: 0;
  margin: 0;
  box-sizing: border-box;

  color: var(--logo-color);
  background: var(--bg-color);

  box-shadow:
    0 10px 15px -3px rgba(0, 0, 0, 0.15),
    0 4px 6px -2px rgba(0, 0, 0, 0.1),
    0 -1px rgba(255, 255, 255, 0.1),
    0 0 0 1px var(--border-color);
  border-radius: 999px;

  cursor: pointer;

  display: flex;
  justify-content: center;
  align-items: center;

  animation: bounceInUp 0.2s ease-out;

  transition: background 0.1s ease;

  &.open {
    background: var(--bg-light-color);
  }

  & .override-indicator {
    position: absolute;
    top: 1px;
    right: 1px;
    width: 8px;
    height: 8px;
    background-color: var(--brand400);
    border-radius: 50%;
    opacity: 0;
    transition: opacity 0.1s ease-in-out;
    box-shadow: inset 0px 1px rgba(255, 255, 255, 0.1);

    &.show {
      opacity: 1;
      animation: gelatine 0.5s;
    }
  }
}

.toolbar-header-button {
  background: none;
  border: none;
  color: var(--dimmed-color);
  cursor: pointer;
  border-radius: 4px;
  transition: background-color 0.1s ease;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 28px;
  width: 28px;

  &:hover {
    background-color: var(--bg-light-color);
    color: var(--text-color);
  }

  &:focus-visible {
    outline: 1px solid #fff;
    outline-offset: 0px;
  }

  & + .button-tooltip {
    pointer-events: none;
    opacity: 0;

    background: var(--bg-color);
    color: var(--text-color);
    padding: 6px 8px;
    border-radius: 4px;
    font-size: 13px;
  }

  &:hover + .button-tooltip {
    opacity: 1;
  }
}

[data-tooltip] {
  position: relative;
}

[data-tooltip]:after {
  content: attr(data-tooltip);
  position: absolute;
  right: 100%;
  top: 0%;
  margin-right: 3px;
  user-select: none;
  pointer-events: none;
  background-color: var(--bg-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  padding: 0px 6px;
  height: 28px;
  line-height: 26px;
  color: var(--text-color);
  font-size: var(--text-small-size);
  font-weight: normal;
  width: max-content;
  display: none;
  box-sizing: border-box;
}

[data-tooltip]:hover:after {
  display: block;
}

```

--------------------------------------------------------------------------------
/packages/browser-sdk/test/mocks/handlers.ts:
--------------------------------------------------------------------------------

```typescript
import { DefaultBodyType, http, HttpResponse, StrictRequest } from "msw";

import { RawFlags } from "../../src/flag/flags";

export const testChannel = "testChannel";

export const flagResponse = {
  success: true,
  features: {
    flagA: {
      isEnabled: true,
      key: "flagA",
      targetingVersion: 1,
      config: undefined,
      ruleEvaluationResults: [false, true],
      missingContextFields: ["field1", "field2"],
    },
    flagB: {
      isEnabled: true,
      targetingVersion: 11,
      key: "flagB",
      config: {
        version: 12,
        key: "gpt3",
        payload: { model: "gpt-something", temperature: 0.5 },
        ruleEvaluationResults: [true, false, false],
        missingContextFields: ["field3"],
      },
    },
  },
};

export const flagsResult = Object.entries(flagResponse.features).reduce(
  (acc, [key, flag]) => {
    acc[key] = {
      ...flag!,
      config: flag.config,
      isEnabledOverride: null,
    };
    return acc;
  },
  {} as RawFlags,
);

function checkRequest(request: StrictRequest<DefaultBodyType>) {
  const url = new URL(request.url);
  const hasKey =
    url.searchParams.get("publishableKey") ||
    request.headers.get("Authorization");

  const hasSdkVersion =
    url.searchParams.get("reflag-sdk-version") ||
    request.headers.get("reflag-sdk-version");

  const valid = hasKey && hasSdkVersion;
  if (!valid) {
    console.log(
      "missing token or sdk: " +
        request.url.toString() +
        " " +
        JSON.stringify(request.headers),
    );
  }
  return valid;
}

const invalidReqResponse = new HttpResponse("missing token or sdk", {
  status: 400,
});

export function getFlags({
  request,
}: {
  request: StrictRequest<DefaultBodyType>;
}) {
  if (!checkRequest(request)) return invalidReqResponse;

  return HttpResponse.json(flagResponse);
}

export const handlers = [
  http.post("https://front.reflag.com/user", async ({ request }) => {
    if (!checkRequest(request)) return invalidReqResponse;

    const data = await request.json();
    if (
      typeof data !== "object" ||
      !data ||
      !data["userId"] ||
      !data["attributes"]
    ) {
      return HttpResponse.error();
    }

    return HttpResponse.json({
      success: true,
    });
  }),
  http.post("https://front.reflag.com/company", async ({ request }) => {
    if (!checkRequest(request)) return invalidReqResponse;
    const data = await request.json();

    if (
      typeof data !== "object" ||
      !data ||
      !data["companyId"] ||
      !data["attributes"]
    ) {
      return HttpResponse.error();
    }

    return HttpResponse.json({
      success: true,
    });
  }),
  http.post("https://front.reflag.com/event", async ({ request }) => {
    if (!checkRequest(request)) return invalidReqResponse;
    const data = await request.json();

    if (typeof data !== "object" || !data || !data["userId"]) {
      return HttpResponse.error();
    }

    return HttpResponse.json({
      success: true,
    });
  }),
  http.post("https://front.reflag.com/features/events", async ({ request }) => {
    if (!checkRequest(request)) return invalidReqResponse;
    const data = await request.json();

    if (typeof data !== "object" || !data || !data["userId"]) {
      return HttpResponse.error();
    }

    return HttpResponse.json({
      success: true,
    });
  }),
  http.post("https://front.reflag.com/feedback", async ({ request }) => {
    if (!checkRequest(request)) return invalidReqResponse;
    const data = await request.json();
    if (
      typeof data !== "object" ||
      !data ||
      !data["userId"] ||
      typeof data["score"] !== "number" ||
      (!data["featureId"] && !data["key"])
    ) {
      return HttpResponse.error();
    }

    return HttpResponse.json({
      success: true,
    });
  }),
  http.get("https://front.reflag.com/features/enabled", getFlags),
  http.get("https://front.reflag.com/features/evaluated", getFlags),
  http.post(
    "https://front.reflag.com/feedback/prompting-init",
    ({ request }) => {
      if (!checkRequest(request)) return invalidReqResponse;

      return HttpResponse.json({ success: true, channel: testChannel });
    },
  ),
  http.get(
    "https://front.reflag.com/feedback/prompting-auth",
    ({ request }) => {
      if (!checkRequest(request)) return invalidReqResponse;
      return HttpResponse.json({ success: true, keyName: "keyName" });
    },
  ),
  http.post(
    "https://livemessaging.bucket.co/keys/keyName/requestToken",
    async ({ request }) => {
      const data = await request.json();
      if (typeof data !== "object") {
        return HttpResponse.error();
      }

      return HttpResponse.json({
        success: true,
        token: "token",
        expires: 1234567890,
      });
    },
  ),
];

```

--------------------------------------------------------------------------------
/packages/eslint-config/base.js:
--------------------------------------------------------------------------------

```javascript
const jsPlugin = require("@eslint/js");
const importsPlugin = require("eslint-plugin-import");
const unusedImportsPlugin = require("eslint-plugin-unused-imports");
const sortImportsPlugin = require("eslint-plugin-simple-import-sort");
const { builtinModules } = require("module");
const globals = require("globals");
const tsPlugin = require("@typescript-eslint/eslint-plugin");
const tsParser = require("@typescript-eslint/parser");
const prettierConfig = require("eslint-config-prettier");
const vuePlugin = require("eslint-plugin-vue");

module.exports = [
  {
    // Blacklisted Folders, including **/node_modules/ and .git/
    ignores: ["build/", "**/gen"],
  },
  {
    // All files
    files: [
      "**/*.js",
      "**/*.cjs",
      "**/*.mjs",
      "**/*.jsx",
      "**/*.ts",
      "**/*.tsx",
      "**/*.vue",
    ],
    plugins: {
      import: importsPlugin,
      "unused-imports": unusedImportsPlugin,
      "simple-import-sort": sortImportsPlugin,
    },
    languageOptions: {
      globals: {
        ...globals.node,
        ...globals.browser,
      },
      parserOptions: {
        // Eslint doesn't supply ecmaVersion in `parser.js` `context.parserOptions`
        // This is required to avoid ecmaVersion < 2015 error or 'import' / 'export' error
        sourceType: "module",
        ecmaVersion: "latest",
        ecmaFeatures: {
          modules: true,
          impliedStrict: true,
          jsx: true,
        },
      },
    },
    settings: {
      react: {
        version: "detect",
      },
      "import/parsers": {
        // Workaround until import supports flat config
        // https://github.com/import-js/eslint-plugin-import/issues/2556
        espree: [".js", ".cjs", ".mjs", ".jsx"],
      },
      "import/resolver": {
        typescript: {
          alwaysTryTypes: true,
        },
      },
    },
    rules: {
      ...jsPlugin.configs.recommended.rules,
      ...importsPlugin.configs.recommended.rules,

      // Imports
      "unused-imports/no-unused-vars": [
        "warn",
        {
          vars: "all",
          varsIgnorePattern: "^_",
          args: "after-used",
          argsIgnorePattern: "^_",
        },
      ],
      "unused-imports/no-unused-imports": ["warn"],
      "import/first": ["warn"],
      "import/newline-after-import": ["warn"],
      "import/no-named-as-default": ["off"],
      "simple-import-sort/exports": ["warn"],
      "simple-import-sort/imports": [
        "warn",
        {
          groups: [
            // Side effect imports.
            ["^\\u0000"],
            // Node.js builtins, react, and third-party packages.
            [
              `^(${builtinModules.join("|")})(/|$)`,
              "^react",
              "^(?!@reflag)@?\\w",
            ],
            // Shared reflag packages.
            ["^@reflag/(.*)$"],
            // Path aliased root, parent imports, and just `..`.
            ["^@/", "^\\.\\.(?!/?$)", "^\\.\\./?$"],
            // Relative imports, same-folder imports, and just `.`.
            ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
            // Style imports.
            ["^.+\\.s?css$"],
          ],
        },
      ],
    },
  },
  {
    // TypeScript files
    files: ["**/*.ts", "**/*.tsx"],
    plugins: {
      "@typescript-eslint": tsPlugin,
      vue: vuePlugin,
    },
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        project: "./tsconfig.eslint.json",
      },
    },
    settings: {
      ...importsPlugin.configs.typescript.settings,
      "import/resolver": {
        ...importsPlugin.configs.typescript.settings["import/resolver"],
        typescript: {
          project: "./tsconfig.json",
        },
      },
    },
    rules: {
      ...importsPlugin.configs.typescript.rules,
      ...tsPlugin.configs["eslint-recommended"].overrides[0].rules,
      ...tsPlugin.configs.recommended.rules,

      // Typescript Specific
      "@typescript-eslint/no-unused-vars": ["off"], // handled by unused-imports
      "@typescript-eslint/explicit-module-boundary-types": ["off"],
      "@typescript-eslint/no-floating-promises": ["error"],
      "@typescript-eslint/switch-exhaustiveness-check": ["warn"],
      "@typescript-eslint/no-non-null-assertion": ["off"],
      "@typescript-eslint/no-empty-function": ["warn"],
      "@typescript-eslint/no-explicit-any": ["off"],
      "@typescript-eslint/no-use-before-define": ["off"],
      "@typescript-eslint/no-shadow": ["warn"],
    },
  },

  {
    files: ["**/*.tsx"],
    rules: {
      "react/prop-types": "off",
    },
  },
  {
    // Prettier Overrides
    files: [
      "**/*.js",
      "**/*.cjs",
      "**/*.mjs",
      "**/*.jsx",
      "**/*.ts",
      "**/*.tsx",
      "**/*.vue",
    ],
    rules: {
      ...prettierConfig.rules,
    },
  },
];

```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/packages/floating-ui-preact-dom/useFloating.ts:
--------------------------------------------------------------------------------

```typescript
import { computePosition } from "@floating-ui/dom";
import {
  useCallback,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "preact/hooks";

import { deepEqual } from "./utils/deepEqual";
import { getDPR } from "./utils/getDPR";
import { roundByDPR } from "./utils/roundByDPR";
import { useLatestRef } from "./utils/useLatestRef";
import type {
  ComputePositionConfig,
  ReferenceType,
  UseFloatingData,
  UseFloatingOptions,
  UseFloatingReturn,
} from "./types";

/**
 * Provides data to position a floating element.
 * @see https://floating-ui.com/docs/react
 */
export function useFloating<RT extends ReferenceType = ReferenceType>(
  options: UseFloatingOptions = {},
): UseFloatingReturn<RT> {
  const {
    placement = "bottom",
    strategy = "absolute",
    middleware = [],
    platform,
    elements: { reference: externalReference, floating: externalFloating } = {},
    transform = true,
    whileElementsMounted,
    open,
  } = options;

  const [data, setData] = useState<UseFloatingData>({
    x: 0,
    y: 0,
    strategy,
    placement,
    middlewareData: {},
    isPositioned: false,
  });

  const [latestMiddleware, setLatestMiddleware] = useState(middleware);

  if (!deepEqual(latestMiddleware, middleware)) {
    setLatestMiddleware(middleware);
  }

  const [_reference, _setReference] = useState<RT | null>(null);
  const [_floating, _setFloating] = useState<HTMLElement | null>(null);

  const setReference = useCallback(
    (node: RT | null) => {
      if (node != referenceRef.current) {
        referenceRef.current = node;
        _setReference(node);
      }
    },
    [_setReference],
  );

  const setFloating = useCallback(
    (node: HTMLElement | null) => {
      if (node !== floatingRef.current) {
        floatingRef.current = node;
        _setFloating(node);
      }
    },
    [_setFloating],
  );

  const referenceEl = (externalReference || _reference) as RT | null;
  const floatingEl = externalFloating || _floating;

  const referenceRef = useRef<RT | null>(null);
  const floatingRef = useRef<HTMLElement | null>(null);
  const dataRef = useRef(data);

  const whileElementsMountedRef = useLatestRef(whileElementsMounted);
  const platformRef = useLatestRef(platform);

  const update = useCallback(() => {
    if (!referenceRef.current || !floatingRef.current) {
      return;
    }

    const config: ComputePositionConfig = {
      placement,
      strategy,
      middleware: latestMiddleware,
    };

    if (platformRef.current) {
      config.platform = platformRef.current;
    }

    /*eslint-disable-next-line @typescript-eslint/no-floating-promises*/
    computePosition(referenceRef.current, floatingRef.current, config).then(
      (positionData) => {
        const fullData = { ...positionData, isPositioned: true };
        if (isMountedRef.current && !deepEqual(dataRef.current, fullData)) {
          dataRef.current = fullData;
          setData(fullData);
        }
      },
    );
  }, [latestMiddleware, placement, strategy, platformRef]);

  useLayoutEffect(() => {
    if (open === false && dataRef.current.isPositioned) {
      dataRef.current.isPositioned = false;
      setData((positionData) => ({ ...positionData, isPositioned: false }));
    }
  }, [open]);

  const isMountedRef = useRef(false);
  useLayoutEffect(() => {
    isMountedRef.current = true;
    return () => {
      isMountedRef.current = false;
    };
  }, []);

  useLayoutEffect(() => {
    if (referenceEl) referenceRef.current = referenceEl;
    if (floatingEl) floatingRef.current = floatingEl;

    if (referenceEl && floatingEl) {
      if (whileElementsMountedRef.current) {
        return whileElementsMountedRef.current(referenceEl, floatingEl, update);
      } else {
        return update();
      }
    }
  }, [referenceEl, floatingEl, update, whileElementsMountedRef]);

  const refs = useMemo(
    () => ({
      reference: referenceRef,
      floating: floatingRef,
      setReference,
      setFloating,
    }),
    [setReference, setFloating],
  );

  const elements = useMemo(
    () => ({ reference: referenceEl, floating: floatingEl }),
    [referenceEl, floatingEl],
  );

  const floatingStyles = useMemo(() => {
    const initialStyles = {
      position: strategy,
      left: 0,
      top: 0,
    };

    if (!elements.floating) {
      return initialStyles;
    }

    const x = roundByDPR(elements.floating, data.x);
    const y = roundByDPR(elements.floating, data.y);

    if (transform) {
      return {
        ...initialStyles,
        transform: `translate(${x}px, ${y}px)`,
        ...(getDPR(elements.floating) >= 1.5 && { willChange: "transform" }),
      };
    }

    return {
      position: strategy,
      left: x,
      top: y,
    };
  }, [strategy, transform, elements.floating, data.x, data.y]);

  return useMemo(
    () => ({
      ...data,
      update,
      refs,
      elements,
      floatingStyles,
    }),
    [data, update, refs, elements, floatingStyles],
  );
}

```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/feedback/ui/StarRating.tsx:
--------------------------------------------------------------------------------

```typescript
import { Fragment, FunctionComponent, h } from "preact";
import { useRef } from "preact/hooks";

import { Dissatisfied } from "../../ui/icons/Dissatisfied";
import { Neutral } from "../../ui/icons/Neutral";
import { Satisfied } from "../../ui/icons/Satisfied";
import { VeryDissatisfied } from "../../ui/icons/VeryDissatisfied";
import { VerySatisfied } from "../../ui/icons/VerySatisfied";
import {
  arrow,
  offset,
  useFloating,
} from "../../ui/packages/floating-ui-preact-dom";

import { FeedbackTranslations } from "./types";

const scores = [
  {
    color: "var(--reflag-feedback-dialog-rating-1-color, #dd6b20)",
    bg: "var(--reflag-feedback-dialog-rating-1-background-color, #fbd38d)",
    icon: <VeryDissatisfied />,
    getLabel: (t: FeedbackTranslations) => t.ScoreVeryDissatisfiedLabel,
    value: 1,
  },
  {
    color: "var(--reflag-feedback-dialog-rating-2-color, #ed8936)",
    bg: "var(--reflag-feedback-dialog-rating-2-background-color, #feebc8)",
    icon: <Dissatisfied />,
    getLabel: (t: FeedbackTranslations) => t.ScoreDissatisfiedLabel,
    value: 2,
  },
  {
    color: "var(--reflag-feedback-dialog-rating-3-color, #787c91)",
    bg: "var(--reflag-feedback-dialog-rating-3-background-color, #e9e9ed)",
    icon: <Neutral />,
    getLabel: (t: FeedbackTranslations) => t.ScoreNeutralLabel,
    value: 3,
  },
  {
    color: "var(--reflag-feedback-dialog-rating-4-color, #48bb78)",
    bg: "var(--reflag-feedback-dialog-rating-4-background-color, #c6f6d5)",
    icon: <Satisfied />,
    getLabel: (t: FeedbackTranslations) => t.ScoreSatisfiedLabel,
    value: 4,
  },
  {
    color: "var(--reflag-feedback-dialog-rating-5-color, #38a169)",
    bg: "var(--reflag-feedback-dialog-rating-5-background-color, #9ae6b4)",
    icon: <VerySatisfied />,
    getLabel: (t: FeedbackTranslations) => t.ScoreVerySatisfiedLabel,
    value: 5,
  },
] as const;

type ScoreNumber = (typeof scores)[number];

export type StarRatingProps = {
  name: string;
  selectedValue?: number;
  onChange?: h.JSX.GenericEventHandler<HTMLInputElement>;
  t: FeedbackTranslations;
};

export const StarRating: FunctionComponent<StarRatingProps> = ({
  t,
  name,
  selectedValue,
  onChange,
}) => {
  return (
    <div class="star-rating">
      <style>
        {scores.map(
          ({ bg, color }, index) => `
            .star-rating-icons > input:nth-of-type(${
              index + 1
            }):checked + .button {
              border-color: ${color};
            }

            .star-rating-icons > input:nth-of-type(${
              index + 1
            }):checked + .button > div {
              background-color: ${bg};
            }

            .star-rating-icons > input:nth-of-type(${
              index + 1
            }):checked ~ input:nth-of-type(${index + 2}) + .button {
              border-left-color: ${color};
            }
          `,
        )}
      </style>
      <div class="star-rating-icons">
        {scores.map((score) => (
          <Score
            key={score.value}
            isSelected={score.value === selectedValue}
            name={name}
            score={score}
            t={t}
            onChange={onChange}
          />
        ))}
      </div>
    </div>
  );
};

const Score = ({
  isSelected,
  name,
  onChange,
  score,
  t,
}: {
  isSelected: boolean;
  name: string;
  onChange?: h.JSX.GenericEventHandler<HTMLInputElement>;
  score: ScoreNumber;
  t: FeedbackTranslations;
}) => {
  const arrowRef = useRef<HTMLDivElement>(null);
  const { refs, floatingStyles, middlewareData } = useFloating({
    placement: "top",
    middleware: [
      offset(4),
      arrow({
        element: arrowRef,
      }),
    ],
  });

  return (
    <>
      <input
        defaultChecked={isSelected}
        id={`reflag-feedback-score-${score.value}`}
        name={name}
        type="radio"
        value={score.value}
        onChange={onChange}
      />
      <label
        ref={refs.setReference}
        aria-label={score.getLabel(t)}
        class="button"
        for={`reflag-feedback-score-${score.value}`}
        style={{ color: score.color }}
      >
        <div
          style={{
            position: "absolute",
            top: 0,
            left: 0,
            width: "100%",
            height: "100%",
            opacity: 0.2, // TODO: fix overflow
            zIndex: 1,
          }}
        />
        <span style={{ zIndex: 2, display: "flex", alignItems: "center" }}>
          {score.icon}
        </span>
      </label>
      <div ref={refs.setFloating} class="button-tooltip" style={floatingStyles}>
        {score.getLabel(t)}
        <div
          ref={arrowRef}
          class="button-tooltip-arrow"
          style={{
            left:
              middlewareData.arrow?.x != null
                ? `${middlewareData.arrow.x}px`
                : "",
            top:
              middlewareData.arrow?.y != null
                ? `${middlewareData.arrow.y}px`
                : "",
          }}
        />
      </div>
    </>
  );
};

```

--------------------------------------------------------------------------------
/packages/browser-sdk/test/prompts.test.ts:
--------------------------------------------------------------------------------

```typescript
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";

import {
  parsePromptMessage,
  processPromptMessage,
} from "../src/feedback/prompts";
import {
  checkPromptMessageCompleted,
  markPromptMessageCompleted,
} from "../src/feedback/promptStorage";

vi.mock("../src/feedback/promptStorage", () => {
  return {
    markPromptMessageCompleted: vi.fn(),
    checkPromptMessageCompleted: vi.fn(),
  };
});

describe("parsePromptMessage", () => {
  test("will not parse invalid messages", () => {
    expect(parsePromptMessage(undefined)).toBeUndefined();
    expect(parsePromptMessage("invalid")).toBeUndefined();
    expect(
      parsePromptMessage({ showAfter: Date.now(), showBefore: Date.now() }),
    ).toBeUndefined();
    expect(
      parsePromptMessage({
        question: "",
        showAfter: Date.now(),
        showBefore: Date.now(),
      }),
    ).toBeUndefined();
    expect(
      parsePromptMessage({
        question: "hello?",
        showBefore: Date.now(),
        promptId: "123",
        featureId: "123",
      }),
    ).toBeUndefined();
    expect(
      parsePromptMessage({
        question: "hello?",
        showAfter: Date.now(),
        promptId: "123",
        featureId: "123",
      }),
    ).toBeUndefined();
    expect(
      parsePromptMessage({
        question: "hello?",
        showAfter: Date.now(),
        showBefore: Date.now(),
      }),
    ).toBeUndefined();
    expect(
      parsePromptMessage({
        question: "hello?",
        showAfter: Date.now(),
        showBefore: Date.now(),
        promptId: "123",
      }),
    ).toBeUndefined();
    expect(
      parsePromptMessage({
        question: "hello?",
        showAfter: Date.now(),
        showBefore: Date.now(),
        featureId: "123",
      }),
    ).toBeUndefined();
  });

  test("will parse valid messages", () => {
    const start = Date.parse("2021-01-01T00:00:00.000Z");
    const end = Date.parse("2021-01-01T10:00:00.000Z");

    expect(
      parsePromptMessage({
        question: "hello?",
        showAfter: start,
        showBefore: end,
        promptId: "123",
        featureId: "456",
      }),
    ).toEqual({
      question: "hello?",
      showAfter: new Date(start),
      showBefore: new Date(end),
      promptId: "123",
      featureId: "456",
    });
  });
});

describe("processPromptMessage", () => {
  const now = Date.now();
  const promptTemplate = {
    question: "hello?",
    promptId: "123",
    featureId: "456",
  };

  beforeEach(() => {
    vi.mocked(checkPromptMessageCompleted).mockReturnValue(false);
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.clearAllMocks();
  });

  test("will not process seen prompts", () => {
    vi.mocked(checkPromptMessageCompleted).mockReturnValue(true);

    const prompt = {
      ...promptTemplate,
      showAfter: new Date(now - 10000),
      showBefore: new Date(now + 10000),
    };

    const showCallback = vi.fn();

    expect(processPromptMessage("user", prompt, showCallback)).toBe(false);

    expect(showCallback).not.toHaveBeenCalled();
    expect(markPromptMessageCompleted).not.toHaveBeenCalled();
  });

  test("will not process expired prompts", () => {
    const prompt = {
      ...promptTemplate,
      showAfter: new Date(now - 10000),
      showBefore: new Date(now - 5000),
    };

    const showCallback = vi.fn();

    expect(processPromptMessage("user", prompt, showCallback)).toBe(false);

    expect(showCallback).not.toHaveBeenCalled();

    expect(markPromptMessageCompleted).not.toHaveBeenCalled();
  });

  test("will process prompts that are ready to be shown", () => {
    const prompt = {
      ...promptTemplate,
      showAfter: new Date(now - 10000),
      showBefore: new Date(now + 10000),
    };

    const showCallback = vi
      .fn()
      .mockImplementation((_a, _b, actionedCallback) => {
        actionedCallback();
      });

    expect(processPromptMessage("user", prompt, showCallback)).toBe(true);
    expect(showCallback).toHaveBeenCalledWith(
      "user",
      prompt,
      expect.any(Function),
    );

    expect(markPromptMessageCompleted).toHaveBeenCalledOnce();
    expect(markPromptMessageCompleted).toBeCalledWith(
      "user",
      "123",
      prompt.showBefore,
    );
  });

  test("will process and delay prompts that are not yet ready to be shown", () => {
    const prompt = {
      ...promptTemplate,
      showAfter: new Date(now + 5000),
      showBefore: new Date(now + 10000),
    };

    const showCallback = vi
      .fn()
      .mockImplementation((_a, _b, actionedCallback) => {
        actionedCallback();
      });

    expect(processPromptMessage("user", prompt, showCallback)).toBe(true);
    expect(showCallback).not.toHaveBeenCalled();
    expect(localStorage.getItem("prompt-user")).toBeNull();

    vi.runAllTimers();
    expect(showCallback).toHaveBeenCalledWith(
      "user",
      prompt,
      expect.any(Function),
    );

    expect(markPromptMessageCompleted).toHaveBeenCalledOnce();
    expect(markPromptMessageCompleted).toBeCalledWith(
      "user",
      "123",
      prompt.showBefore,
    );
  });
});

```

--------------------------------------------------------------------------------
/packages/cli/commands/mcp.ts:
--------------------------------------------------------------------------------

```typescript
import { select } from "@inquirer/prompts";
import chalk from "chalk";
import { Command } from "commander";
import { parse as parseJSON, stringify as stringifyJSON } from "comment-json";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, relative } from "node:path";
import ora, { type Ora } from "ora";

import {
  ConfigPaths,
  getServersConfig,
  resolveConfigPath,
  SupportedEditor,
  SupportedEditors,
} from "../services/mcp.js";
import { configStore } from "../stores/config.js";
import { handleError } from "../utils/errors.js";
import { fileExists } from "../utils/file.js";
import { configScopeOption, editorOption } from "../utils/options.js";

export const mcpAction = async (options: {
  editor?: SupportedEditor;
  scope?: "local" | "global";
}) => {
  const config = configStore.getConfig();
  let spinner: Ora | undefined;
  let selectedEditor = options.editor;

  // Select Editor/Client
  if (!selectedEditor) {
    selectedEditor = await select<SupportedEditor>({
      message: "Which editor do you want to configure?",
      choices: SupportedEditors.map((value) => ({
        value,
        name: ConfigPaths[value].name,
      })),
    });
  }

  // Determine Config Path
  const projectPath = configStore.getProjectPath();
  const globalPath = resolveConfigPath(selectedEditor, false);
  const localPath = resolveConfigPath(selectedEditor, true);
  const fullLocalPath = localPath ? join(projectPath, localPath) : undefined;

  if (!globalPath) {
    throw new Error(`Unsupported platform for editor: ${selectedEditor}`);
  }

  let configPathType: "global" | "local" = "global";
  if (fullLocalPath) {
    if (options.scope) {
      configPathType = options.scope;
    } else {
      configPathType = await select<"global" | "local">({
        message: "Configure global or project-local settings?",
        choices: [
          {
            name: `Local (${relative(projectPath, fullLocalPath)})`,
            value: "local",
          },
          { name: `Global (${globalPath})`, value: "global" },
        ],
      });
    }
  }
  const configPath = configPathType === "local" ? fullLocalPath! : globalPath;
  const displayConfigPath =
    configPathType === "local" ? relative(projectPath, configPath) : configPath;

  // Read/Parse Config File
  spinner = ora(
    `Reading configuration file: ${chalk.cyan(displayConfigPath)}...`,
  ).start();

  let editorConfig: any = {};
  if (await fileExists(configPath)) {
    const content = await readFile(configPath, "utf-8");
    // Attempt to parse JSON, handle potential comments if needed
    try {
      editorConfig = parseJSON(content);
    } catch {
      spinner.fail(
        `Failed to parse configuration file ${chalk.cyan(displayConfigPath)}.`,
      );
    }
    spinner.succeed(
      `Read configuration file ${chalk.cyan(displayConfigPath)}.`,
    );
  } else {
    spinner.info("Configuration file not found, will create a new one.");
    editorConfig = {}; // Initialize empty config if file doesn't exist
  }

  // Ensure MCP servers object exists
  const serversConfig = getServersConfig(
    editorConfig,
    selectedEditor,
    configPathType,
  );

  // Check for existing Reflag servers
  const existingReflagEntries = Object.keys(serversConfig).filter((key) =>
    /reflag/i.test(key),
  );

  // Prompt for Add/Update
  let targetEntryKey: string;
  const defaultNewKey = `Reflag`;

  if (existingReflagEntries.length === 0) {
    targetEntryKey = defaultNewKey;
    console.log(`Adding new MCP server entry: ${chalk.cyan(targetEntryKey)}`);
  } else {
    const choices = [
      { name: `Add: ${defaultNewKey}`, value: "add_new" },
      ...existingReflagEntries.map((key) => ({
        name: `Update: ${key}`,
        value: key,
      })),
    ];

    const choice = await select({
      message: "Add a new MCP server or update an existing one?",
      choices,
    });

    if (choice === "add_new") {
      targetEntryKey = defaultNewKey;
      console.log(`Adding new MCP server entry: ${chalk.cyan(targetEntryKey)}`);
    } else {
      targetEntryKey = choice;
      console.log(
        `Updating existing MCP server entry: ${chalk.cyan(targetEntryKey)}`,
      );
    }
  }

  // Construct the MCP endpoint URL
  const newEntryValue = {
    url: config.apiUrl + "/mcp",
  };

  // Update Config Object
  serversConfig[targetEntryKey] = newEntryValue;

  // Write Config File
  spinner = ora(
    `Writing configuration to ${chalk.cyan(displayConfigPath)}...`,
  ).start();

  try {
    // Ensure the directory exists before writing
    await mkdir(dirname(configPath), { recursive: true });
    const configString = stringifyJSON(editorConfig, null, 2);

    await writeFile(configPath, configString);
    spinner.succeed(
      `Configuration updated successfully in ${chalk.cyan(displayConfigPath)}.`,
    );

    console.log(
      chalk.grey(
        "You may need to restart your editor for changes to take effect.",
      ),
    );
  } catch (error) {
    spinner.fail(
      `Failed to write configuration file ${chalk.cyan(displayConfigPath)}.`,
    );

    handleError(error, "MCP Configuration");
  }
};

export function registerMcpCommand(cli: Command) {
  cli
    .command("mcp")
    .description("Configure Reflag's remote MCP server for your AI assistant.")
    .addOption(editorOption)
    .addOption(configScopeOption)
    .action(mcpAction);
}

```

--------------------------------------------------------------------------------
/packages/react-sdk/dev/nextjs-flag-demo/app/page.tsx:
--------------------------------------------------------------------------------

```typescript
import Image from "next/image";
import { Flags } from "@/components/Flags";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
        <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
          <a
            className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
            href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            target="_blank"
            rel="noopener noreferrer"
          >
            By{" "}
            <Image
              src="/vercel.svg"
              alt="Vercel Logo"
              className="dark:invert"
              width={100}
              height={24}
              priority
            />
          </a>
        </div>
      </div>

      <div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
        <Image
          className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
          src="/next.svg"
          alt="Next.js Logo"
          width={180}
          height={37}
          priority
        />
      </div>

      <Flags />

      <div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
        <a
          href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className="mb-3 text-2xl font-semibold">
            Docs{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className="m-0 max-w-[30ch] text-sm opacity-50">
            Find in-depth information about Next.js features and API.
          </p>
        </a>

        <a
          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className="mb-3 text-2xl font-semibold">
            Learn{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className="m-0 max-w-[30ch] text-sm opacity-50">
            Learn about Next.js in an interactive course with&nbsp;quizzes!
          </p>
        </a>

        <a
          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className="mb-3 text-2xl font-semibold">
            Templates{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className="m-0 max-w-[30ch] text-sm opacity-50">
            Explore starter templates for Next.js.
          </p>
        </a>

        <a
          href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className="mb-3 text-2xl font-semibold">
            Deploy{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
            Instantly deploy your Next.js site to a shareable URL with Vercel.
          </p>
        </a>
      </div>
    </main>
  );
}

```

--------------------------------------------------------------------------------
/packages/react-sdk/dev/nextjs-bootstrap-demo/app/page.tsx:
--------------------------------------------------------------------------------

```typescript
import Image from "next/image";
import { Flags } from "@/components/Flags";

export default async function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
        <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
          <a
            className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
            href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            target="_blank"
            rel="noopener noreferrer"
          >
            By{" "}
            <Image
              src="/vercel.svg"
              alt="Vercel Logo"
              className="dark:invert w-auto h-auto"
              width={100}
              height={24}
              priority
            />
          </a>
        </div>
      </div>

      <div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
        <Image
          className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
          src="/next.svg"
          alt="Next.js Logo"
          width={180}
          height={37}
          priority
        />
      </div>

      <Flags />

      <div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
        <a
          href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className="mb-3 text-2xl font-semibold">
            Docs{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className="m-0 max-w-[30ch] text-sm opacity-50">
            Find in-depth information about Next.js features and API.
          </p>
        </a>

        <a
          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className="mb-3 text-2xl font-semibold">
            Learn{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className="m-0 max-w-[30ch] text-sm opacity-50">
            Learn about Next.js in an interactive course with&nbsp;quizzes!
          </p>
        </a>

        <a
          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className="mb-3 text-2xl font-semibold">
            Templates{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className="m-0 max-w-[30ch] text-sm opacity-50">
            Explore starter templates for Next.js.
          </p>
        </a>

        <a
          href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          target="_blank"
          rel="noopener noreferrer"
        >
          <h2 className="mb-3 text-2xl font-semibold">
            Deploy{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
          <p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
            Instantly deploy your Next.js site to a shareable URL with Vercel.
          </p>
        </a>
      </div>
    </main>
  );
}

```

--------------------------------------------------------------------------------
/packages/node-sdk/test/flusher.test.ts:
--------------------------------------------------------------------------------

```typescript
import { constants } from "os";
import {
  afterEach,
  beforeEach,
  describe,
  expect,
  it,
  MockInstance,
  vi,
} from "vitest";

import { subscribe } from "../src/flusher";

describe("flusher", () => {
  const mockExit = vi
    .spyOn(process, "exit")
    .mockImplementation((() => undefined) as any);

  const mockConsoleError = vi
    .spyOn(console, "error")
    .mockImplementation(() => undefined);

  const mockProcessOn = vi
    .spyOn(process, "on")
    .mockImplementation((_, __) => process);

  const mockProcessPrependListener = (
    vi.spyOn(process, "prependListener") as unknown as MockInstance<
      [event: NodeJS.Signals, listener: NodeJS.SignalsListener],
      NodeJS.Process
    >
  ).mockImplementation((_, __) => process);

  const mockListenerCount = vi
    .spyOn(process, "listenerCount")
    .mockReturnValue(0);

  function timedCallback(ms: number) {
    return vi.fn().mockImplementation(
      () =>
        new Promise((resolve) => {
          setTimeout(resolve, ms);
        }),
    );
  }

  function getHandler(eventName: string, prepended = false) {
    return prepended
      ? mockProcessPrependListener.mock.calls.filter(
          ([evt]) => evt === eventName,
        )[0][1]
      : mockProcessOn.mock.calls.filter(([evt]) => evt === eventName)[0][1];
  }

  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.resetAllMocks();
  });

  describe("signal handling", () => {
    const signals = ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"] as const;

    describe.each(signals)("signal %s", (signal) => {
      it("should handle signal with no existing listeners", async () => {
        mockListenerCount.mockReturnValue(0);
        const callback = vi.fn().mockResolvedValue(undefined);

        subscribe(callback);
        expect(mockProcessOn).toHaveBeenCalledWith(
          signal,
          expect.any(Function),
        );

        getHandler(signal)(signal);
        await vi.runAllTimersAsync();

        expect(callback).toHaveBeenCalledTimes(1);
        expect(mockExit).toHaveBeenCalledWith(0x80 + constants.signals[signal]);
      });

      it("should prepend handler when listeners exist", async () => {
        mockListenerCount.mockReturnValue(1);
        const callback = vi.fn().mockResolvedValue(undefined);

        subscribe(callback);

        expect(mockProcessPrependListener).toHaveBeenCalledWith(
          signal,
          expect.any(Function),
        );

        getHandler(signal, true)(signal);

        expect(callback).toHaveBeenCalledTimes(1);
        expect(mockExit).not.toHaveBeenCalled();
      });
    });
  });

  describe("beforeExit handling", () => {
    it("should call callback on beforeExit", async () => {
      const callback = vi.fn().mockResolvedValue(undefined);

      subscribe(callback);

      getHandler("beforeExit")();

      expect(callback).toHaveBeenCalledTimes(1);
    });

    it("should not call callback multiple times", async () => {
      const callback = vi.fn().mockResolvedValue(undefined);

      subscribe(callback);

      getHandler("beforeExit")();
      getHandler("beforeExit")();

      expect(callback).toHaveBeenCalledTimes(1);
    });
  });

  describe("timeout handling", () => {
    it("should handle timeout when callback takes too long", async () => {
      subscribe(timedCallback(2000), 1000);

      getHandler("beforeExit")();

      await vi.advanceTimersByTimeAsync(1000);

      expect(mockConsoleError).toHaveBeenCalledWith(
        "[Reflag SDK] Timeout while flushing events on process exit.",
      );
    });

    it("should not timeout when callback completes in time", async () => {
      subscribe(timedCallback(500), 1000);

      getHandler("beforeExit")();
      await vi.advanceTimersByTimeAsync(500);

      expect(mockConsoleError).not.toHaveBeenCalled();
    });
  });

  describe("exit state handling", () => {
    it("should log error if exit occurs before flushing starts", () => {
      subscribe(timedCallback(0));

      getHandler("exit")();

      expect(mockConsoleError).toHaveBeenCalledWith(
        "[Reflag SDK] Failed to finalize the flushing of events on process exit.",
      );
    });

    it("should log error if exit occurs before flushing completes", async () => {
      subscribe(timedCallback(2000));
      getHandler("beforeExit")();

      await vi.advanceTimersByTimeAsync(1000);

      getHandler("exit")();

      expect(mockConsoleError).toHaveBeenCalledWith(
        "[Reflag SDK] Failed to finalize the flushing of events on process exit.",
      );
    });

    it("should not log error if flushing completes before exit", async () => {
      subscribe(timedCallback(500));

      getHandler("beforeExit")();
      await vi.advanceTimersByTimeAsync(500);

      getHandler("exit")();

      expect(mockConsoleError).not.toHaveBeenCalled();
    });

    it("should handle callback errors gracefully", async () => {
      subscribe(vi.fn().mockRejectedValue(new Error("Test error")));

      getHandler("beforeExit")();
      await vi.runAllTimersAsync();

      expect(mockConsoleError).toHaveBeenCalledWith(
        "[Reflag SDK] An error occurred while flushing events on process exit.",
        expect.any(Error),
      );
    });
  });

  it("should run the callback only once", async () => {
    const callback = vi.fn().mockResolvedValue(undefined);

    subscribe(callback);

    getHandler("SIGINT")("SIGINT");
    getHandler("beforeExit")();

    await vi.runAllTimersAsync();

    expect(callback).toHaveBeenCalledTimes(1);
  });
});

```

--------------------------------------------------------------------------------
/packages/cli/stores/config.ts:
--------------------------------------------------------------------------------

```typescript
import { Ajv, ValidateFunction } from "ajv";
import {
  assign as assignJSON,
  parse as parseJSON,
  stringify as stringifyJSON,
} from "comment-json";
import equal from "fast-deep-equal";
import { findUp } from "find-up";
import { readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";

import {
  CONFIG_FILE_NAME,
  DEFAULT_API_URL,
  DEFAULT_BASE_URL,
  DEFAULT_TYPES_OUTPUT,
  MODULE_ROOT,
  SCHEMA_URL,
} from "../utils/constants.js";
import { ConfigValidationError, handleError } from "../utils/errors.js";
import { stripTrailingSlash } from "../utils/urls.js";
import { current as currentVersion } from "../utils/version.js";

export const typeFormats = ["react", "node"] as const;
export type TypeFormat = (typeof typeFormats)[number];

export type TypesOutput = {
  path: string;
  format: TypeFormat;
};

type Config = {
  $schema: string;
  baseUrl: string;
  apiUrl: string;
  appId: string | undefined;
  typesOutput: TypesOutput[];
};

const defaultConfig: Config = {
  $schema: SCHEMA_URL,
  baseUrl: DEFAULT_BASE_URL,
  apiUrl: DEFAULT_API_URL,
  appId: undefined,
  typesOutput: [{ path: DEFAULT_TYPES_OUTPUT, format: "react" }],
};

// Helper to normalize typesOutput to array format
export function normalizeTypesOutput(
  output?: string | TypesOutput[],
): TypesOutput[] | undefined {
  if (!output) return undefined;
  if (typeof output === "string") {
    return [{ path: output, format: "react" }];
  }
  return output;
}

class ConfigStore {
  protected config: Config = { ...defaultConfig };
  protected configPath: string | undefined;
  protected projectPath: string | undefined;
  protected clientVersion: string | undefined;
  protected validateConfig: ValidateFunction | undefined;

  async initialize() {
    await this.createValidator();
    await this.loadConfigFile();
  }

  protected async createValidator() {
    try {
      // Using current config store file, resolve the schema.json path
      const schemaPath = join(MODULE_ROOT, "schema.json");
      const content = await readFile(schemaPath, "utf-8");
      const parsed = parseJSON(content) as unknown as Config;

      const ajv = new Ajv();
      this.validateConfig = ajv.compile(parsed);
    } catch {
      handleError(new Error("Failed to load the config schema"), "Config");
    }
  }

  protected async loadConfigFile() {
    if (!this.validateConfig) {
      handleError(new Error("Failed to load the config schema"), "Config");
    }

    // Load the client version from the module's package.json metadata
    try {
      const { version } = await currentVersion();
      this.clientVersion = version;
    } catch {
      // Should not be the case, but ignore if no package.json is found
    }

    try {
      const projectMetadataPath = await findUp("package.json");
      this.configPath = await findUp(CONFIG_FILE_NAME);
      this.projectPath = dirname(
        this.configPath ?? projectMetadataPath ?? process.cwd(),
      );

      if (!this.configPath) return;

      const content = await readFile(this.configPath, "utf-8");
      const parsed = parseJSON(content) as unknown as Partial<Config>;

      // Normalize values
      if (parsed.baseUrl)
        parsed.baseUrl = stripTrailingSlash(parsed.baseUrl.trim());
      if (parsed.apiUrl)
        parsed.apiUrl = stripTrailingSlash(parsed.apiUrl.trim());
      if (parsed.typesOutput?.length)
        parsed.typesOutput = normalizeTypesOutput(parsed.typesOutput);

      if (!this.validateConfig!(parsed)) {
        handleError(
          new ConfigValidationError(this.validateConfig!.errors),
          "Config",
        );
      }

      this.config = assignJSON(this.config, parsed);
    } catch {
      // No config file found
    }
  }

  /**
   * Create a new config file with initial values.
   * @param overwrite If true, overwrites existing config file. Defaults to false
   */
  async saveConfigFile(overwrite = false) {
    const configWithoutDefaults: Partial<Config> = assignJSON({}, this.config);

    // Only include non-default values and $schema
    for (const untypedKey in configWithoutDefaults) {
      const key = untypedKey as keyof Config;
      if (
        !["$schema"].includes(key) &&
        equal(configWithoutDefaults[key], defaultConfig[key])
      ) {
        delete configWithoutDefaults[key];
      }
    }

    const configJSON = stringifyJSON(configWithoutDefaults, null, 2);

    if (this.configPath && !overwrite) {
      throw new Error("Config file already exists");
    }

    if (this.configPath) {
      await writeFile(this.configPath, configJSON);
    } else {
      // Write to the project path
      const packageJSONPath = await findUp("package.json");
      this.projectPath = dirname(packageJSONPath ?? process.cwd());
      this.configPath = join(this.projectPath, CONFIG_FILE_NAME);
      await writeFile(this.configPath, configJSON);
    }
  }

  getConfig(): Config;
  getConfig<K extends keyof Config>(key: K): Config[K];
  getConfig<K extends keyof Config>(key?: K) {
    return key ? this.config?.[key] : this.config;
  }

  getConfigPath() {
    return this.configPath;
  }

  getClientVersion() {
    return this.clientVersion;
  }

  getProjectPath() {
    return this.projectPath ?? process.cwd();
  }

  setConfig(newConfig: Partial<Config>) {
    // Update the config with new values skipping undefined values
    for (const untypedKey in newConfig) {
      const key = untypedKey as keyof Config;
      if (newConfig[key] === undefined) continue;
      (this.config as any)[key] = newConfig[key];
    }
  }
}

export const configStore = new ConfigStore();

```

--------------------------------------------------------------------------------
/packages/openfeature-browser-provider/src/index.ts:
--------------------------------------------------------------------------------

```typescript
import {
  ErrorCode,
  EvaluationContext,
  JsonValue,
  OpenFeatureEventEmitter,
  Provider,
  ProviderMetadata,
  ProviderStatus,
  ResolutionDetails,
  StandardResolutionReasons,
  TrackingEventDetails,
} from "@openfeature/web-sdk";

import { Flag, InitOptions, ReflagClient } from "@reflag/browser-sdk";

export type ContextTranslationFn = (
  context?: EvaluationContext,
) => Record<string, any>;

export function defaultContextTranslator(
  context?: EvaluationContext,
): Record<string, any> {
  if (!context) return {};
  return {
    user: {
      id: context.targetingKey ?? context["userId"]?.toString(),
      email: context["email"]?.toString(),
      name: context["name"]?.toString(),
      avatar: context["avatar"]?.toString(),
      country: context["country"]?.toString(),
    },
    company: {
      id: context["companyId"]?.toString(),
      name: context["companyName"]?.toString(),
      plan: context["companyPlan"]?.toString(),
      avatar: context["companyAvatar"]?.toString(),
    },
  };
}

export class ReflagBrowserSDKProvider implements Provider {
  readonly metadata: ProviderMetadata = {
    name: "reflag-browser-provider",
  };

  private _client?: ReflagClient;

  private readonly _clientOptions: InitOptions;
  private readonly _contextTranslator: ContextTranslationFn;

  public events = new OpenFeatureEventEmitter();

  private _status: ProviderStatus = ProviderStatus.NOT_READY;

  set status(status: ProviderStatus) {
    this._status = status;
  }

  get status() {
    return this._status;
  }

  get client() {
    return this._client;
  }

  constructor({
    contextTranslator,
    ...opts
  }: InitOptions & { contextTranslator?: ContextTranslationFn }) {
    this._clientOptions = opts;
    this._contextTranslator = contextTranslator || defaultContextTranslator;
  }

  async initialize(context?: EvaluationContext): Promise<void> {
    const client = new ReflagClient({
      ...this._clientOptions,
      ...this._contextTranslator(context),
    });

    try {
      await client.initialize();
      this.status = ProviderStatus.READY;
      this._client = client;
    } catch {
      this.status = ProviderStatus.ERROR;
    }
  }

  onClose(): Promise<void> {
    if (this._client) {
      return this._client?.stop();
    }
    return Promise.resolve();
  }

  async onContextChange(
    _oldContext: EvaluationContext,
    newContext: EvaluationContext,
  ): Promise<void> {
    await this.initialize(newContext);
  }

  private resolveFlag<T extends JsonValue>(
    flagKey: string,
    defaultValue: T,
    resolveFn: (feature: Flag) => ResolutionDetails<T>,
  ): ResolutionDetails<T> {
    if (!this._client) {
      return {
        value: defaultValue,
        reason: StandardResolutionReasons.DEFAULT,
        errorCode: ErrorCode.PROVIDER_NOT_READY,
        errorMessage: "Reflag client not initialized",
      } satisfies ResolutionDetails<T>;
    }

    const features = this._client.getFlags();
    if (flagKey in features) {
      return resolveFn(this._client.getFlag(flagKey));
    }

    return {
      value: defaultValue,
      reason: StandardResolutionReasons.DEFAULT,
      errorCode: ErrorCode.FLAG_NOT_FOUND,
      errorMessage: `Flag ${flagKey} not found`,
    };
  }

  resolveBooleanEvaluation(flagKey: string, defaultValue: boolean) {
    return this.resolveFlag(flagKey, defaultValue, (feature) => {
      return {
        value: feature.isEnabled,
        variant: feature.config.key,
        reason: StandardResolutionReasons.TARGETING_MATCH,
      };
    });
  }

  resolveNumberEvaluation(_flagKey: string, defaultValue: number) {
    return {
      value: defaultValue,
      reason: StandardResolutionReasons.ERROR,
      errorCode: ErrorCode.GENERAL,
      errorMessage:
        "Reflag doesn't support this method. Use `resolveObjectEvaluation` instead.",
    };
  }

  resolveStringEvaluation(
    flagKey: string,
    defaultValue: string,
  ): ResolutionDetails<string> {
    return this.resolveFlag(flagKey, defaultValue, (feature) => {
      if (!feature.config.key) {
        return {
          value: defaultValue,
          reason: StandardResolutionReasons.DEFAULT,
        };
      }

      return {
        value: feature.config.key as string,
        variant: feature.config.key,
        reason: StandardResolutionReasons.TARGETING_MATCH,
      };
    });
  }

  resolveObjectEvaluation<T extends JsonValue>(
    flagKey: string,
    defaultValue: T,
  ) {
    return this.resolveFlag(flagKey, defaultValue, (feature) => {
      const expType = typeof defaultValue;

      const payloadType = typeof feature.config.payload;

      if (
        feature.config.payload === undefined ||
        feature.config.payload === null ||
        payloadType !== expType
      ) {
        return {
          value: defaultValue,
          reason: StandardResolutionReasons.ERROR,
          variant: feature.config.key,
          errorCode: ErrorCode.TYPE_MISMATCH,
          errorMessage: `Expected remote config payload of type \`${expType}\` but got \`${payloadType}\`.`,
        };
      }

      return {
        value: feature.config.payload,
        variant: feature.config.key,
        reason: StandardResolutionReasons.TARGETING_MATCH,
      };
    });
  }

  track(
    trackingEventName: string,
    _context?: EvaluationContext,
    trackingEventDetails?: TrackingEventDetails,
  ): void {
    if (!this._client) {
      this._clientOptions.logger?.error("client not initialized");
    }

    this._client
      ?.track(trackingEventName, trackingEventDetails)
      .catch((e: any) => {
        this._clientOptions.logger?.error("error tracking event", e);
      });
  }
}

```

--------------------------------------------------------------------------------
/packages/node-sdk/src/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { createHash, Hash } from "crypto";

import { IdType, Logger, LogLevel } from "./types";

/**
 * Assert that the given condition is `true`.
 *
 * @param condition - The condition to check.
 * @param message - The error message to throw.
 **/
export function ok(condition: boolean, message: string): asserts condition {
  if (!condition) {
    throw new Error(`validation failed: ${message}`);
  }
}
/**
 * Assert that the given values is a valid user/company id
 **/
export function idOk(id: IdType, entity: string) {
  ok(
    (typeof id === "string" && id.length > 0) || typeof id === "number",
    `${entity} must be a string or number if given`,
  );
}

/**
 * Check if the given item is an object.
 *
 * @param item - The item to check.
 * @returns `true` if the item is an object, `false` otherwise.
 **/
export function isObject(item: any): item is Record<string, any> {
  return (item && typeof item === "object" && !Array.isArray(item)) || false;
}

export type LogFn = (message: string, ...args: any[]) => void;
export function decorate(prefix: string, fn: LogFn): LogFn {
  return (message: string, ...args: any[]) => {
    fn(`${prefix} ${message}`, ...args);
  };
}

export function applyLogLevel(logger: Logger, logLevel: LogLevel) {
  switch (logLevel?.toLocaleUpperCase()) {
    case "DEBUG":
      return {
        debug: decorate("[debug]", logger.debug),
        info: decorate("[info]", logger.info),
        warn: decorate("[warn]", logger.warn),
        error: decorate("[error]", logger.error),
      };
    case "INFO":
      return {
        debug: () => void 0,
        info: decorate("[info]", logger.info),
        warn: decorate("[warn]", logger.warn),
        error: decorate("[error]", logger.error),
      };
    case "WARN":
      return {
        debug: () => void 0,
        info: () => void 0,
        warn: decorate("[warn]", logger.warn),
        error: decorate("[error]", logger.error),
      };
    case "ERROR":
      return {
        debug: () => void 0,
        info: () => void 0,
        warn: () => void 0,
        error: decorate("[error]", logger.error),
      };
    default:
      throw new Error(`invalid log level: ${logLevel}`);
  }
}

/**
 * Decorate the messages of a given logger with the given prefix.
 *
 * @param prefix - The prefix to add to log messages.
 * @param logger - The logger to decorate.
 * @returns The decorated logger.
 **/
export function decorateLogger(prefix: string, logger: Logger): Logger {
  ok(typeof prefix === "string", "prefix must be a string");
  ok(typeof logger === "object", "logger must be an object");

  return {
    debug: decorate(prefix, logger.debug),
    info: decorate(prefix, logger.info),
    warn: decorate(prefix, logger.warn),
    error: decorate(prefix, logger.error),
  };
}

/** Merge two objects, skipping `undefined` values.
 *
 * @param target - The target object.
 * @param source - The source object.
 * @returns The merged object.
 **/
export function mergeSkipUndefined<T extends object, U extends object>(
  target: T,
  source: U,
): T & U {
  const newTarget = { ...target };
  for (const key in source) {
    if (source[key] === undefined) {
      continue;
    }
    (newTarget as any)[key] = source[key];
  }
  return newTarget as T & U;
}

function updateSha1Hash(hash: Hash, value: any) {
  if (value === null) {
    hash.update("null");
  } else {
    switch (typeof value) {
      case "object":
        if (Array.isArray(value)) {
          for (const item of value) {
            updateSha1Hash(hash, item);
          }
        } else {
          for (const key of Object.keys(value).sort()) {
            hash.update(key);
            updateSha1Hash(hash, value[key]);
          }
        }
        break;
      case "string":
        hash.update(value);
        break;
      case "number":
      case "boolean":
      case "symbol":
      case "bigint":
      case "function":
        hash.update(value.toString());
        break;
      case "undefined":
      default:
        break;
    }
  }
}

/** Hash an object using SHA1.
 *
 * @param obj - The object to hash.
 *
 * @returns The SHA1 hash of the object.
 **/
export function hashObject(obj: Record<string, any>): string {
  ok(isObject(obj), "obj must be an object");

  const hash = createHash("sha1");
  updateSha1Hash(hash, obj);

  return hash.digest("base64");
}

export function once<T extends () => ReturnType<T>>(
  fn: T,
): () => ReturnType<T> {
  let called = false;
  let returned: ReturnType<T> | undefined;
  return function (): ReturnType<T> {
    if (called) {
      return returned!;
    }
    returned = fn();
    called = true;
    return returned;
  };
}

export class TimeoutError extends Error {
  constructor(timeoutMs: number) {
    super(`Operation timed out after ${timeoutMs}ms`);
    this.name = "TimeoutError";
  }
}

/**
 * Wraps a promise with a timeout. If the promise doesn't resolve within the specified
 * timeout, it will reject with a timeout error. The original promise will still
 * continue to execute but its result will be ignored.
 *
 * @param promise - The promise to wrap with a timeout
 * @param timeoutMs - The timeout in milliseconds
 * @returns A promise that resolves with the original promise result or rejects with a timeout error
 * @throws {Error} If the timeout is reached before the promise resolves
 **/
export function withTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number,
): Promise<T> {
  ok(timeoutMs > 0, "timeout must be a positive number");

  return new Promise((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      reject(new TimeoutError(timeoutMs));
    }, timeoutMs);

    promise
      .then((result) => {
        resolve(result);
      })
      .catch((error) => {
        reject(error);
      })
      .finally(() => {
        clearTimeout(timeoutId);
      });
  });
}

```

--------------------------------------------------------------------------------
/packages/cli/commands/flags.ts:
--------------------------------------------------------------------------------

```typescript
import { input } from "@inquirer/prompts";
import chalk from "chalk";
import { Command } from "commander";
import { relative } from "node:path";
import ora, { Ora } from "ora";

import { App, getApp, getOrg } from "../services/bootstrap.js";
import { createFlag, Flag, listFlags } from "../services/flags.js";
import { configStore } from "../stores/config.js";
import {
  handleError,
  MissingAppIdError,
  MissingEnvIdError,
} from "../utils/errors.js";
import {
  genFlagKey,
  genTypes,
  indentLines,
  KeyFormatPatterns,
  writeTypesToFile,
} from "../utils/gen.js";
import {
  appIdOption,
  flagKeyOption,
  flagNameArgument,
  typesFormatOption,
  typesOutOption,
} from "../utils/options.js";
import { baseUrlSuffix, featureUrl } from "../utils/urls.js";

type CreateFlagOptions = {
  key?: string;
};

export const createFlagAction = async (
  name: string | undefined,
  { key }: CreateFlagOptions,
) => {
  const { baseUrl, appId } = configStore.getConfig();
  let spinner: Ora | undefined;

  if (!appId) {
    handleError(new MissingAppIdError(), "Flags Create");
  }

  let app: App;
  try {
    app = getApp(appId);
  } catch (error) {
    handleError(error, "Flags Create");
  }

  const production = app.environments.find((e) => e.isProduction);

  try {
    const org = getOrg();
    console.log(
      `Creating flag for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
    );

    if (!name) {
      name = await input({
        message: "New flag name:",
        validate: (text) => text.length > 0 || "Name is required.",
      });
    }

    if (!key) {
      const keyFormat = org.featureKeyFormat;
      const keyValidator = KeyFormatPatterns[keyFormat];
      key = await input({
        message: "New flag key:",
        default: genFlagKey(name, keyFormat),
        validate: (str) => keyValidator.regex.test(str) || keyValidator.message,
      });
    }

    spinner = ora(`Creating flag...`).start();
    const flag = await createFlag(appId, { name, key });

    spinner.succeed(
      `Created flag ${chalk.cyan(flag.name)} with key ${chalk.cyan(flag.key)}:`,
    );
    if (production) {
      console.log(
        indentLines(chalk.magenta(featureUrl(baseUrl, production, flag))),
      );
    }
  } catch (error) {
    spinner?.fail("Flag creation failed.");
    handleError(error, "Flags Create");
  }
};

export const listFlagsAction = async () => {
  const { baseUrl, appId } = configStore.getConfig();
  let spinner: Ora | undefined;

  if (!appId) {
    handleError(new MissingAppIdError(), "Flags Create");
  }

  try {
    const app = getApp(appId);
    const production = app.environments.find((e) => e.isProduction);
    if (!production) {
      handleError(new MissingEnvIdError(), "Flags Types");
    }

    spinner = ora(
      `Loading flags of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`,
    ).start();

    const flagsResponse = await listFlags(appId, {
      envId: production.id,
    });

    spinner.succeed(
      `Loaded flags of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
    );

    console.table(
      flagsResponse.data.map(({ key, name, stage }) => ({
        name,
        key,
        stage: stage?.name,
      })),
    );
  } catch (error) {
    spinner?.fail("Loading flags failed.");
    handleError(error, "Flags List");
  }
};

export const generateTypesAction = async () => {
  const { baseUrl, appId } = configStore.getConfig();
  const typesOutput = configStore.getConfig("typesOutput");

  let spinner: Ora | undefined;
  let flags: Flag[] = [];

  if (!appId) {
    handleError(new MissingAppIdError(), "Flags Types");
  }

  let app: App;
  try {
    app = getApp(appId);
  } catch (error) {
    handleError(error, "Flags Types");
  }

  const production = app.environments.find((e) => e.isProduction);
  if (!production) {
    handleError(new MissingEnvIdError(), "Flags Types");
  }

  try {
    spinner = ora(
      `Loading flags of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`,
    ).start();

    flags = await listFlags(appId, {
      envId: production.id,
      includeRemoteConfigs: true,
    }).then((res) => res.data);

    spinner.succeed(
      `Loaded flags of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`,
    );
  } catch (error) {
    spinner?.fail("Loading flags failed.");
    handleError(error, "Flags Types");
  }

  try {
    spinner = ora(`Generating flag types...`).start();
    const projectPath = configStore.getProjectPath();

    // Generate types for each output configuration
    for (const output of typesOutput) {
      const types = genTypes(flags, output.format);

      const outPath = await writeTypesToFile(types, output.path, projectPath);
      spinner.succeed(
        `Generated ${output.format} types in ${chalk.cyan(relative(projectPath, outPath))}.`,
      );
    }
  } catch (error) {
    spinner?.fail("Type generation failed.");
    handleError(error, "Flags Types");
  }
};

export function registerFlagCommands(cli: Command) {
  const flagsCommand = new Command("flags").description("Manage flags.");

  flagsCommand
    .command("create")
    .description("Create a new flag.")
    .addOption(appIdOption)
    .addOption(flagKeyOption)
    .addArgument(flagNameArgument)
    .action(createFlagAction);

  flagsCommand
    .command("list")
    .alias("ls")
    .description("List all flags.")
    .addOption(appIdOption)
    .action(listFlagsAction);

  flagsCommand
    .command("types")
    .description("Generate flag types.")
    .addOption(appIdOption)
    .addOption(typesOutOption)
    .addOption(typesFormatOption)
    .action(generateTypesAction);

  // Update the config with the cli override values
  flagsCommand.hook("preAction", (_, command) => {
    const { appId, out, format } = command.opts();
    configStore.setConfig({
      appId,
      typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
    });
  });

  cli.addCommand(flagsCommand);
}

```

--------------------------------------------------------------------------------
/packages/cli/utils/json.ts:
--------------------------------------------------------------------------------

```typescript
export type JSONPrimitive =
  | number
  | string
  | boolean
  | null
  | JSONPrimitive[]
  | { [key: string]: JSONPrimitive };

export type PrimitiveAST = { kind: "primitive"; type: string };
export type ArrayAST = { kind: "array"; elementType: TypeAST };
export type ObjectAST = {
  kind: "object";
  properties: { key: string; type: TypeAST; optional: boolean }[];
};
export type UnionAST = { kind: "union"; types: TypeAST[] };

// Type AST to represent TypeScript types
export type TypeAST = PrimitiveAST | ArrayAST | ObjectAST | UnionAST;

// Convert JSON value to TypeAST
export function toTypeAST(value: JSONPrimitive, path: string[] = []): TypeAST {
  if (value === null) return { kind: "primitive", type: "null" };

  if (Array.isArray(value)) {
    if (value.length === 0) {
      return {
        kind: "array",
        elementType: { kind: "primitive", type: "any" },
      };
    }

    // Process all elements in the array instead of just the first one
    const elementTypes = value.map((item, index) =>
      toTypeAST(item, [...path, index.toString()]),
    );

    return {
      kind: "array",
      elementType: mergeTypeASTs(elementTypes),
    };
  }

  if (typeof value === "object") {
    return {
      kind: "object",
      properties: Object.entries(value).map(([key, val]) => ({
        key,
        type: toTypeAST(val, [...path, key]),
        optional: false,
      })),
    };
  }

  return { kind: "primitive", type: typeof value };
}

// Merge multiple TypeASTs into one
export function mergeTypeASTs(types: TypeAST[]): TypeAST {
  if (types.length === 0) return { kind: "primitive", type: "any" };
  if (types.length === 1) return types[0];

  // Group ASTs by kind
  const byKind = {
    union: types.filter((t) => t.kind === "union"),
    primitive: types.filter((t) => t.kind === "primitive"),
    array: types.filter((t) => t.kind === "array"),
    object: types.filter((t) => t.kind === "object"),
  };

  // Create a union for mixed kinds
  const hasMixedKinds =
    byKind.union.length > 0 || // If we have any unions, treat it as mixed kinds
    (byKind.primitive.length > 0 &&
      (byKind.array.length > 0 || byKind.object.length > 0)) ||
    (byKind.array.length > 0 && byKind.object.length > 0);

  if (hasMixedKinds) {
    // If there are existing unions, flatten them into the current union
    if (byKind.union.length > 0) {
      // Flatten existing unions and collect types by category
      const flattenedTypes: TypeAST[] = [];
      const objectsToMerge: ObjectAST[] = [...byKind.object];
      const arraysToMerge: ArrayAST[] = [...byKind.array];

      // Add primitives directly
      flattenedTypes.push(...byKind.primitive);

      // Process union types
      for (const unionType of byKind.union) {
        for (const type of unionType.types) {
          if (type.kind === "object") {
            objectsToMerge.push(type);
          } else if (type.kind === "array") {
            arraysToMerge.push(type);
          } else {
            flattenedTypes.push(type);
          }
        }
      }

      // Merge objects and arrays if they exist
      if (objectsToMerge.length > 0) {
        flattenedTypes.push(mergeTypeASTs(objectsToMerge));
      }

      if (arraysToMerge.length > 0) {
        flattenedTypes.push(mergeTypeASTs(arraysToMerge));
      }

      return { kind: "union", types: flattenedTypes };
    }

    return { kind: "union", types };
  }

  // Handle primitives
  if (byKind.primitive.length === types.length) {
    const uniqueTypes = [...new Set(byKind.primitive.map((p) => p.type))];
    return uniqueTypes.length === 1
      ? { kind: "primitive", type: uniqueTypes[0] }
      : {
          kind: "union",
          types: uniqueTypes.map((type) => ({ kind: "primitive", type })),
        };
  }

  // Merge arrays
  if (byKind.array.length === types.length) {
    return {
      kind: "array",
      elementType: mergeTypeASTs(byKind.array.map((a) => a.elementType)),
    };
  }

  // Merge objects
  if (byKind.object.length === types.length) {
    // Get all unique property keys
    const allKeys = [
      ...new Set(
        byKind.object.flatMap((obj) => obj.properties.map((p) => p.key)),
      ),
    ];

    // Merge properties with same keys
    const mergedProperties = allKeys.map((key) => {
      const props = byKind.object
        .map((obj) => obj.properties.find((p) => p.key === key))
        .filter((obj) => !!obj);

      return {
        key,
        type: mergeTypeASTs(props.map((p) => p.type)),
        optional: byKind.object.some(
          (obj) => !obj.properties.some((p) => p.key === key),
        ),
      };
    });

    return { kind: "object", properties: mergedProperties };
  }

  // Fallback
  return { kind: "primitive", type: "any" };
}

// Stringify TypeAST to TypeScript type declaration
export function stringifyTypeAST(ast: TypeAST, nestLevel = 0): string {
  const indent = " ".repeat(nestLevel * 2);
  const nextIndent = " ".repeat((nestLevel + 1) * 2);

  switch (ast.kind) {
    case "primitive":
      return ast.type;

    case "array":
      return `(${stringifyTypeAST(ast.elementType, nestLevel)})[]`;

    case "object":
      if (ast.properties.length === 0) return "{}";

      return `{\n${ast.properties
        .map(({ key, optional, type }) => {
          return `${nextIndent}${quoteKey(key)}${optional ? "?" : ""}: ${stringifyTypeAST(
            type,
            nestLevel + 1,
          )}`;
        })
        .join(",\n")}\n${indent}}`;

    case "union":
      if (ast.types.length === 0) return "any";
      if (ast.types.length === 1)
        return stringifyTypeAST(ast.types[0], nestLevel);
      return ast.types
        .map((type) => stringifyTypeAST(type, nestLevel))
        .join(" | ");
  }
}

export function quoteKey(key: string): string {
  return /[^a-zA-Z0-9_]/.test(key) || /^[0-9]/.test(key) ? `"${key}"` : key;
}

// Convert JSON array to TypeScript type
export function JSONToType(json: JSONPrimitive[]): string | null {
  if (!json.length) return null;

  return stringifyTypeAST(mergeTypeASTs(json.map((item) => toTypeAST(item))));
}

```

--------------------------------------------------------------------------------
/packages/node-sdk/test/batch-buffer.test.ts:
--------------------------------------------------------------------------------

```typescript
import {
  afterAll,
  beforeAll,
  beforeEach,
  describe,
  expect,
  it,
  vi,
} from "vitest";

import BatchBuffer from "../src/batch-buffer";
import { BATCH_INTERVAL_MS, BATCH_MAX_SIZE } from "../src/config";
import { Logger } from "../src/types";

describe("BatchBuffer", () => {
  const mockFlushHandler = vi.fn();

  const mockLogger: Logger = {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe("constructor", () => {
    it("should throw an error if options are invalid", () => {
      expect(() => new BatchBuffer(null as any)).toThrow(
        "options must be an object",
      );

      expect(() => new BatchBuffer("bad" as any)).toThrow(
        "options must be an object",
      );

      expect(
        () => new BatchBuffer({ flushHandler: null as any } as any),
      ).toThrow("flushHandler must be a function");

      expect(
        () => new BatchBuffer({ flushHandler: "not a function" } as any),
      ).toThrow("flushHandler must be a function");

      expect(
        () =>
          new BatchBuffer({
            flushHandler: mockFlushHandler,
            logger: "string",
          } as any),
      ).toThrow("logger must be an object");

      expect(
        () =>
          new BatchBuffer({
            flushHandler: mockFlushHandler,
            maxSize: -1,
          } as any),
      ).toThrow("maxSize must be greater than 0");
    });

    it("should initialize with specified values", () => {
      const buffer = new BatchBuffer({
        flushHandler: mockFlushHandler,
        maxSize: 22,
        intervalMs: 33,
      });

      expect(buffer).toEqual({
        buffer: [],
        flushHandler: mockFlushHandler,
        timer: null,
        intervalMs: 33,
        logger: undefined,
        maxSize: 22,
      });
    });

    it("should initialize with default values if not provided", () => {
      const buffer = new BatchBuffer({ flushHandler: mockFlushHandler });
      expect(buffer).toEqual({
        buffer: [],
        flushHandler: mockFlushHandler,
        intervalMs: BATCH_INTERVAL_MS,
        maxSize: BATCH_MAX_SIZE,
        timer: null,
      });
    });
  });

  describe("add", () => {
    it("should add item to the buffer and flush immediately if maxSize is reached", async () => {
      const buffer = new BatchBuffer({
        flushHandler: mockFlushHandler,
        maxSize: 1,
      });

      await buffer.add("item1");

      expect(mockFlushHandler).toHaveBeenCalledWith(["item1"]);
      expect(mockFlushHandler).toHaveBeenCalledTimes(1);
    });

    it("should set a flush timer if buffer does not reach maxSize", async () => {
      vi.useFakeTimers();

      const buffer = new BatchBuffer({
        flushHandler: mockFlushHandler,
        maxSize: 2,
        intervalMs: 1000,
      });

      await buffer.add("item1");
      expect(mockFlushHandler).not.toHaveBeenCalled();

      vi.advanceTimersByTime(1000);
      expect(mockFlushHandler).toHaveBeenCalledWith(["item1"]);
      expect(mockFlushHandler).toHaveBeenCalledTimes(1);

      vi.useRealTimers();
    });
  });

  describe("flush", () => {
    it("should not do anything if there are no items to flush", async () => {
      const buffer = new BatchBuffer({
        flushHandler: mockFlushHandler,
        logger: mockLogger,
      });

      await buffer.flush();

      expect(mockFlushHandler).not.toHaveBeenCalled();

      expect(mockLogger.debug).toHaveBeenCalledWith(
        "buffer is empty. nothing to flush",
      );
    });

    it("calling flush simultaneously should only flush data once", async () => {
      let itemsFlushed = 0;
      const buffer = new BatchBuffer({
        flushHandler: async (items) => {
          itemsFlushed += items.length;
          await new Promise((resolve) => setTimeout(resolve, 100));
          mockFlushHandler();
        },
        logger: mockLogger,
      });

      await buffer.add("item1");
      await Promise.all([buffer.flush(), buffer.flush()]);
      expect(itemsFlushed).toBe(1);
    });

    it("should flush buffer", async () => {
      const buffer = new BatchBuffer({
        flushHandler: mockFlushHandler,
        logger: mockLogger,
      });

      await buffer.add("item1");
      await buffer.flush();

      expect(mockFlushHandler).toHaveBeenCalledWith(["item1"]);
      await buffer.flush();

      expect(mockFlushHandler).toHaveBeenCalledTimes(1);
    });

    it("should log correctly during flush", async () => {
      const buffer = new BatchBuffer({
        flushHandler: mockFlushHandler,
        logger: mockLogger,
      });

      await buffer.add("item1");
      await buffer.flush();

      expect(mockLogger.info).toHaveBeenCalledWith("flushed buffered items", {
        count: 1,
      });
    });
  });

  describe("timer logic", () => {
    beforeAll(() => {
      vi.useFakeTimers();
    });

    afterAll(() => {
      vi.useRealTimers();
    });

    beforeEach(() => {
      vi.clearAllTimers();
      mockFlushHandler.mockReset();
    });

    it("should start the normal timer when adding first item", async () => {
      const buffer = new BatchBuffer({
        flushHandler: mockFlushHandler,
        logger: mockLogger,
        intervalMs: 100,
      });

      expect(buffer["timer"]).toBeNull();
      await buffer.add("item1");

      expect(buffer["timer"]).toBeDefined();

      await vi.advanceTimersByTimeAsync(100);
      expect(mockFlushHandler).toHaveBeenCalledTimes(1);

      expect(buffer["timer"]).toBeNull();

      expect(mockLogger.info).toHaveBeenCalledWith("flushed buffered items", {
        count: 1,
      });
    });

    it("should stop the normal timer if flushed manually", async () => {
      const buffer = new BatchBuffer({
        flushHandler: mockFlushHandler,
        logger: mockLogger,
        intervalMs: 100,
        maxSize: 2,
      });

      await buffer.add("item1");
      await buffer.add("item2");

      expect(buffer["timer"]).toBeNull();

      expect(mockLogger.info).toHaveBeenCalledWith("flushed buffered items", {
        count: 2,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/packages/openfeature-node-provider/src/index.ts:
--------------------------------------------------------------------------------

```typescript
import {
  ErrorCode,
  EvaluationContext,
  JsonValue,
  OpenFeatureEventEmitter,
  Paradigm,
  Provider,
  ResolutionDetails,
  ServerProviderStatus,
  StandardResolutionReasons,
  TrackingEventDetails,
} from "@openfeature/server-sdk";

import {
  ClientOptions,
  Context as ReflagContext,
  ReflagClient,
} from "@reflag/node-sdk";

type ProviderOptions = ClientOptions & {
  contextTranslator?: (context: EvaluationContext) => ReflagContext;
};

export const defaultContextTranslator = (
  context: EvaluationContext,
): ReflagContext => {
  const user = {
    id: context.targetingKey ?? context["userId"]?.toString(),
    name: context["name"]?.toString(),
    email: context["email"]?.toString(),
    avatar: context["avatar"]?.toString(),
    country: context["country"]?.toString(),
  };

  const company = {
    id: context["companyId"]?.toString(),
    name: context["companyName"]?.toString(),
    avatar: context["companyAvatar"]?.toString(),
    plan: context["companyPlan"]?.toString(),
  };

  return {
    user,
    company,
  };
};

export class ReflagNodeProvider implements Provider {
  public readonly events = new OpenFeatureEventEmitter();

  private _client: ReflagClient;

  private contextTranslator: (context: EvaluationContext) => ReflagContext;

  public runsOn: Paradigm = "server";

  public status: ServerProviderStatus = ServerProviderStatus.NOT_READY;

  public metadata = {
    name: "reflag-node",
  };

  get client() {
    return this._client;
  }

  constructor({ contextTranslator, ...opts }: ProviderOptions) {
    this._client = new ReflagClient(opts);
    this.contextTranslator = contextTranslator ?? defaultContextTranslator;
  }

  public async initialize(): Promise<void> {
    await this._client.initialize();
    this.status = ServerProviderStatus.READY;
  }

  private resolveFlag<T extends JsonValue>(
    flagKey: string,
    defaultValue: T,
    context: ReflagContext,
    resolveFn: (
      feature: ReturnType<typeof this._client.getFlag>,
    ) => Promise<ResolutionDetails<T>>,
  ): Promise<ResolutionDetails<T>> {
    if (this.status !== ServerProviderStatus.READY) {
      return Promise.resolve({
        value: defaultValue,
        reason: StandardResolutionReasons.ERROR,
        errorCode: ErrorCode.PROVIDER_NOT_READY,
        errorMessage: "Reflag client not initialized",
      });
    }

    if (!context.user?.id) {
      return Promise.resolve({
        value: defaultValue,
        reason: StandardResolutionReasons.ERROR,
        errorCode: ErrorCode.INVALID_CONTEXT,
        errorMessage: "At least a user ID is required",
      });
    }

    const featureDefs = this._client.getFlagDefinitions();
    if (featureDefs.some(({ key }) => key === flagKey)) {
      return resolveFn(this._client.getFlag(context, flagKey));
    }

    return Promise.resolve({
      value: defaultValue,
      reason: StandardResolutionReasons.ERROR,
      errorCode: ErrorCode.FLAG_NOT_FOUND,
      errorMessage: `Flag ${flagKey} not found`,
    });
  }

  resolveBooleanEvaluation(
    flagKey: string,
    defaultValue: boolean,
    context: EvaluationContext,
  ): Promise<ResolutionDetails<boolean>> {
    return this.resolveFlag(
      flagKey,
      defaultValue,
      this.contextTranslator(context),
      (feature) => {
        return Promise.resolve({
          value: feature.isEnabled,
          variant: feature.config?.key,
          reason: StandardResolutionReasons.TARGETING_MATCH,
        });
      },
    );
  }

  resolveStringEvaluation(
    flagKey: string,
    defaultValue: string,
    context: EvaluationContext,
  ): Promise<ResolutionDetails<string>> {
    return this.resolveFlag(
      flagKey,
      defaultValue,
      this.contextTranslator(context),
      (feature) => {
        if (!feature.config.key) {
          return Promise.resolve({
            value: defaultValue,
            reason: StandardResolutionReasons.DEFAULT,
          });
        }

        return Promise.resolve({
          value: feature.config.key as string,
          variant: feature.config.key,
          reason: StandardResolutionReasons.TARGETING_MATCH,
        });
      },
    );
  }

  resolveNumberEvaluation(
    _flagKey: string,
    defaultValue: number,
  ): Promise<ResolutionDetails<number>> {
    return Promise.resolve({
      value: defaultValue,
      reason: StandardResolutionReasons.ERROR,
      errorCode: ErrorCode.GENERAL,
      errorMessage:
        "Reflag doesn't support this method. Use `resolveObjectEvaluation` instead.",
    });
  }

  resolveObjectEvaluation<T extends JsonValue>(
    flagKey: string,
    defaultValue: T,
    context: EvaluationContext,
  ): Promise<ResolutionDetails<T>> {
    return this.resolveFlag(
      flagKey,
      defaultValue,
      this.contextTranslator(context),
      (feature) => {
        const expType = typeof defaultValue;
        const payloadType = typeof feature.config.payload;

        if (
          feature.config.payload === undefined ||
          feature.config.payload === null ||
          payloadType !== expType
        ) {
          return Promise.resolve({
            value: defaultValue,
            variant: feature.config.key,
            reason: StandardResolutionReasons.ERROR,
            errorCode: ErrorCode.TYPE_MISMATCH,
            errorMessage: `Expected remote config payload of type \`${expType}\` but got \`${payloadType}\`.`,
          });
        }

        return Promise.resolve({
          value: feature.config.payload,
          variant: feature.config.key,
          reason: StandardResolutionReasons.TARGETING_MATCH,
        });
      },
    );
  }

  track(
    trackingEventName: string,
    context?: EvaluationContext,
    trackingEventDetails?: TrackingEventDetails,
  ): void {
    const translatedContext = context
      ? this.contextTranslator(context)
      : undefined;

    const userId = translatedContext?.user?.id;
    if (!userId) {
      this._client.logger?.warn("No user ID provided for tracking event");
      return;
    }

    void this._client.track(String(userId), trackingEventName, {
      attributes: trackingEventDetails,
      companyId: translatedContext?.company?.id?.toString(),
    });
  }

  public async onClose(): Promise<void> {
    await this._client.flush();
  }
}

```

--------------------------------------------------------------------------------
/packages/browser-sdk/src/ui/Dialog.tsx:
--------------------------------------------------------------------------------

```typescript
import { MiddlewareData, Placement } from "@floating-ui/dom";
import { Fragment, FunctionComponent, h, Ref } from "preact";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";

import {
  arrow,
  autoUpdate,
  flip,
  offset,
  shift,
  useFloating,
} from "./packages/floating-ui-preact-dom";
import styles from "./Dialog.css?inline";
import { Position } from "./types";
import { parseUnanchoredPosition } from "./utils";

type CssPosition = Partial<
  Record<"top" | "left" | "right" | "bottom", number | string>
>;

export interface OpenDialogOptions {
  /**
   * Control the placement and behavior of the dialog.
   */
  position: Position;

  strategy?: "fixed" | "absolute";

  isOpen: boolean;
  close: () => void;
  onDismiss?: () => void;

  containerId: string;

  showArrow?: boolean;

  children?: preact.ComponentChildren;
}

export function useDialog({
  onClose,
  onOpen,
  initialValue = false,
}: {
  onClose?: () => void;
  onOpen?: () => void;
  initialValue?: boolean;
} = {}) {
  const [isOpen, setIsOpen] = useState<boolean>(initialValue);

  const open = useCallback(() => {
    setIsOpen(true);
    onOpen?.();
  }, [onOpen]);
  const close = useCallback(() => {
    setIsOpen(false);
    onClose?.();
  }, [onClose]);
  const toggle = useCallback(() => {
    if (isOpen) onClose?.();
    else onOpen?.();
    setIsOpen((prev) => !prev);
  }, [isOpen, onClose, onOpen]);

  return {
    isOpen,
    open,
    close,
    toggle,
  };
}

export const Dialog: FunctionComponent<OpenDialogOptions> = ({
  position,
  isOpen,
  close,
  onDismiss,
  containerId,
  strategy,
  children,
  showArrow = true,
}) => {
  const arrowRef = useRef<HTMLDivElement>(null);
  const dialogRef = useRef<HTMLDialogElement>(null);

  const anchor = position.type === "POPOVER" ? position.anchor : null;
  const placement =
    position.type === "POPOVER" ? position.placement : undefined;

  const {
    refs,
    floatingStyles,
    middlewareData,
    placement: actualPlacement,
  } = useFloating({
    elements: {
      reference: anchor,
    },
    strategy,
    transform: false,
    placement,
    whileElementsMounted: autoUpdate,
    middleware: [
      flip({
        padding: 10,
        mainAxis: true,
        crossAxis: true,
      }),
      shift(),
      offset(8),
      arrow({
        element: arrowRef,
      }),
    ],
  });

  let unanchoredPosition: CssPosition = {};
  if (position.type === "DIALOG") {
    unanchoredPosition = parseUnanchoredPosition(position);
  }

  const dismiss = useCallback(() => {
    close();
    onDismiss?.();
  }, [close, onDismiss]);

  useEffect(() => {
    // Only enable 'quick dismiss' for popovers
    if (position.type === "MODAL" || position.type === "DIALOG") return;

    const escapeHandler = (e: KeyboardEvent) => {
      if (e.key == "Escape") {
        dismiss();
      }
    };

    const clickOutsideHandler = (e: MouseEvent) => {
      if (
        !(e.target instanceof Element) ||
        !e.target.closest(`#${containerId}`)
      ) {
        dismiss();
      }
    };

    const observer = new MutationObserver((mutations) => {
      if (position.anchor === null) return;

      mutations.forEach((mutation) => {
        const removedNodes = Array.from(mutation.removedNodes);
        const hasBeenRemoved = removedNodes.some((node) => {
          return node === position.anchor || node.contains(position.anchor);
        });

        if (hasBeenRemoved) {
          close();
        }
      });
    });

    window.addEventListener("mousedown", clickOutsideHandler);
    window.addEventListener("keydown", escapeHandler);
    observer.observe(document.body, {
      subtree: true,
      childList: true,
    });

    return () => {
      window.removeEventListener("mousedown", clickOutsideHandler);
      window.removeEventListener("keydown", escapeHandler);
      observer.disconnect();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps -- anchor only exists in popover
  }, [position.type, close, (position as any).anchor, dismiss, containerId]);

  function setDiagRef(node: HTMLDialogElement | null) {
    refs.setFloating(node);
    dialogRef.current = node;
  }

  useEffect(() => {
    if (!dialogRef.current) return;
    if (isOpen && !dialogRef.current.hasAttribute("open")) {
      dialogRef.current[position.type === "MODAL" ? "showModal" : "show"]();
    }
    if (!isOpen && dialogRef.current.hasAttribute("open")) {
      dialogRef.current.close();
    }
  }, [dialogRef, isOpen, position.type]);

  const classes = [
    "dialog",
    position.type === "MODAL"
      ? "modal"
      : position.type === "POPOVER"
        ? "anchored"
        : `unanchored unanchored-${position.placement}`,
    actualPlacement,
  ].join(" ");

  return (
    <>
      <style dangerouslySetInnerHTML={{ __html: styles }} />
      <dialog
        ref={setDiagRef}
        class={classes}
        style={anchor ? floatingStyles : unanchoredPosition}
      >
        {children && <Fragment>{children}</Fragment>}

        {anchor && showArrow && (
          <DialogArrow
            arrowData={middlewareData?.arrow}
            arrowRef={arrowRef}
            placement={actualPlacement}
          />
        )}
      </dialog>
    </>
  );
};

function DialogArrow({
  arrowData,
  arrowRef,
  placement,
}: {
  arrowData: MiddlewareData["arrow"];
  arrowRef: Ref<HTMLDivElement>;
  placement: Placement;
}) {
  const { x: arrowX, y: arrowY } = arrowData ?? {};

  const staticSide =
    {
      top: "bottom",
      right: "left",
      bottom: "top",
      left: "right",
    }[placement.split("-")[0]] || "bottom";

  const arrowStyles = {
    left: arrowX != null ? `${arrowX}px` : "",
    top: arrowY != null ? `${arrowY}px` : "",
    right: "",
    bottom: "",
    [staticSide]: "-4px",
  };
  return (
    <div
      ref={arrowRef}
      class={["arrow", placement].join(" ")}
      style={arrowStyles}
    />
  );
}

export function DialogHeader({
  children,
  innerRef,
}: {
  children: preact.ComponentChildren;
  innerRef?: Ref<HTMLElement>;
}) {
  return (
    <header ref={innerRef} class="dialog-header">
      {children}
    </header>
  );
}

export function DialogContent({
  children,
  innerRef,
}: {
  children: preact.ComponentChildren;
  innerRef?: Ref<HTMLDivElement>;
}) {
  return (
    <div ref={innerRef} class="dialog-content">
      {children}
    </div>
  );
}

```
Page 3/7FirstPrevNextLast