> For the complete documentation index, see [llms.txt](https://docs.winga.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.winga.me/sdks/javascript.md).

# @winga/js

`@winga/js` is the core SDK. It runs in any modern Node.js or browser environment, loads your flags over REST, then keeps them live with a realtime stream. It is fully typed and **never throws in normal operation** — every flag read returns a value, even when Winga is unreachable.

***

### Installation

```bash
npm install @winga/js
# or
pnpm add @winga/js
# or
yarn add @winga/js
```

***

### Initialisation

`Winga(options)` is a plain factory function — **not** a constructor. Do not use `new`. It performs an immediate `GET /v1/flags?env=<environment>` to load the initial snapshot, then opens an SSE connection for realtime updates.

```typescript
import { Winga } from '@winga/js'

const ff = Winga({
  apiKey: 'proj_live_xxx',
  environment: 'production',
})
```

#### `WingaOptions`

| Option         | Type                        | Default                | Notes                                                                                                            |
| -------------- | --------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `apiKey`       | `string`                    | **Required**           | Your project's eval key. Only `proj_live_` keys ship today.                                                      |
| `environment`  | `string`                    | **Required**           | Must match an environment name in your project (e.g. `production`).                                              |
| `baseUrl`      | `string`                    | `https://api.winga.me` | Override the API base URL. Appends `/v1/flags` and `/v1/stream`.                                                 |
| `onError`      | `(err: WingaError) => void` | *none*                 | Called on every transport or client error. The argument is always a `WingaError`, so `err.code` is safe to read. |
| `readyTimeout` | `number`                    | `5000`                 | Max milliseconds `ready()` waits for the initial load. `0` resolves immediately without waiting.                 |

***

### Core API

A `WingaClient` exposes seven methods. None of them throw.

#### `isEnabled(key)`

Returns the boolean state of a flag. Returns `false` for an unknown flag or while the initial load is in flight.

```typescript
if (ff.isEnabled('new-checkout')) {
  showNewCheckout()
}
```

#### `getValue<T>(key, defaultValue)`

Reads a typed flag value with a required fallback. Returns `T`, never `undefined`. A runtime shape guard compares the server value against `defaultValue`; on a mismatch it fires `onError` with a `TYPE_MISMATCH` `WingaError` and returns the fallback.

```typescript
const banner = ff.getValue<string>('banner-text', 'Welcome!')
```

#### `getAll()`

Returns every flag in the current environment as `Record<string, FlagState>`, useful for debugging or server-side hydration.

```typescript
const all = ff.getAll()
```

#### `on('change', listener)` / `off('change', listener)`

Subscribe to realtime flag changes. The event is `{ key: string; state: FlagState | undefined }`. `state` is `undefined` when the flag was **deleted** from the snapshot — guard before reading `state.enabled`.

```typescript
const listener = (event) => {
  if (event.state === undefined) {
    console.log('Flag removed:', event.key)
    return
  }
  console.log('Flag changed:', event.key, event.state.enabled)
}

ff.on('change', listener)
// Pass the same reference to remove it:
ff.off('change', listener)
```

#### `ready()`

Resolves once the initial flag load has completed (or after `readyTimeout`). **It never rejects** — on a failed load it fires `onError` and then resolves so your code is never blocked. Await it server-side before your first read.

```typescript
await ff.ready()

if (ff.isEnabled('new-checkout')) {
  // ...
}
```

In the browser you usually do not need to await `ready()` — reads return their defaults until the first snapshot arrives, then your `on('change', ...)` listeners fire.

#### `dispose()`

Tears down the SSE connection, clears all listeners, and empties the cache. After `dispose()`, reads return defaults and `on()` routes a `CLIENT_DISPOSED` error to `onError`.

```typescript
ff.dispose()
```

***

### Error handling

The SDK never throws. All errors route to the optional `onError` callback as `WingaError` instances, and flags fall back to their default on every error path.

```typescript
import { Winga, WingaError } from '@winga/js'

const ff = Winga({
  apiKey: 'proj_live_xxx',
  environment: 'production',
  onError: (err: WingaError) => {
    // err.code is safe to read without an instanceof check
    console.error('Winga error:', err.code, err.message)
  },
})
```

#### `WingaError`

| Property  | Type                  | Description                                                |
| --------- | --------------------- | ---------------------------------------------------------- |
| `code`    | `WingaErrorCode`      | One of the codes below.                                    |
| `message` | `string`              | Human-readable sentence describing the failure.            |
| `cause`   | `unknown`             | The underlying error or response object.                   |
| `status`  | `number \| undefined` | The HTTP status code, when the error came from a response. |

#### `WingaErrorCode`

| Code              | When it fires                                                                          |
| ----------------- | -------------------------------------------------------------------------------------- |
| `AUTH`            | API key missing, invalid, or expired (401 response).                                   |
| `NETWORK`         | `fetch` rejected (offline / DNS failure); also fires when `ready()` times out.         |
| `SSE_DISCONNECT`  | Realtime stream interrupted (non-auth, non-forbidden).                                 |
| `POLLING_ERROR`   | The polling fallback's `GET /v1/flags` returned a non-2xx response.                    |
| `CLIENT_DISPOSED` | `on()` was called after `dispose()`.                                                   |
| `FORBIDDEN`       | 403 response — the key lacks permission for this environment.                          |
| `RATE_LIMITED`    | 429 response.                                                                          |
| `MALFORMED`       | The server returned an unexpected response shape.                                      |
| `HTTP_ERROR`      | A non-2xx response not covered by a more specific code; `err.status` holds the code.   |
| `TYPE_MISMATCH`   | `getValue<T>` shape guard failed — the server value shape differs from `defaultValue`. |

***

### Realtime channel

After the initial REST load, the transport opens a `GET /v1/stream?env=<environment>` SSE connection, authenticated with the same `X-Winga-Key` header. On every change the server emits one `data:` frame containing the environment's **full flag map** as `Record<string, FlagState>` — not a diff, and the same shape `GET /v1/flags` returns. Each frame triggers your `on('change', ...)` listeners.

#### Offline and degraded behaviour

| Scenario                             | SDK behaviour                                                                             |
| ------------------------------------ | ----------------------------------------------------------------------------------------- |
| Initial load, Winga unreachable      | Fires `onError(NETWORK)` after `readyTimeout`; `ready()` resolves; flags return defaults. |
| SSE drops mid-session (< 3 failures) | Reconnects with exponential backoff (1s base, 30s max, ±25% jitter).                      |
| SSE fails 3 times in a row           | Falls back to polling `GET /v1/flags` every 900 ms; fires `onError(SSE_DISCONNECT)`.      |
| Server-side RTDB listener drop       | Server ends the stream; the SDK treats it as `SSE_DISCONNECT` and reconnects.             |
| Flag missing from the payload        | That flag returns its `defaultValue`.                                                     |
| Invalid API key                      | Fires `onError(AUTH)`; all flags return defaults. Never throws.                           |

***

### Transport internals

`createTransport`, `TransportHandle`, and `TransportOptions` are exported from `packages/sdk/src/transport.ts` for unit-test use only. They are marked `@internal` and are **not part of the public API** — always import `Winga` from `@winga/js`.

***

### Next steps

* Using React? See the [`@winga/react`](/sdks/react.md) reference.
* Not on JavaScript? Hit the [Evaluation API](/api-reference/evaluation-api.md) directly.
