Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ Single test runs use Vitest's CLI directly: `yarn test-unit path/to/file.test.ts
1. `tsc` — emits **declarations only** (`emitDeclarationOnly: true`) to `dist/types`. `rootDir` is `src/`.
2. `scripts/bundle.mjs` (esbuild) — produces three bundles:
- `dist/cjs/index.node.js` (Node CJS, externalizes deps + Node builtins)
- `dist/cjs/index.browser.js` (browser CJS, externalizes deps except those zeroed in `package.json#browser`)
- `dist/cjs/index.browser.js` (browser CJS)
- `dist/esm/index.mjs` (browser ESM)

`package.json#exports` routes consumers to the right bundle by condition: `node` → node-cjs, `browser`/`react-native` → browser-cjs (require) or esm (import), default → esm. The `browser` field zeroes Node-only deps (`crypto`, `https`, `jsonwebtoken`, `ws`, `zlib`) so they tree-shake out of browser/RN builds. If you add a dep that's Node-only, add it to `browser` so it doesn't leak into browser/RN bundles.
`package.json#exports` routes consumers to the right bundle by condition: `node` → node-cjs, `browser`/`react-native` → browser-cjs (require) or esm (import), default → esm. There is **no `package.json#browser` field** — it used to zero Node-only deps (`crypto`, `https`, `jsonwebtoken`, `ws`, `zlib`) for browser/RN builds, but the SDK no longer imports any of them (`src/index.ts` is platform-agnostic: global `WebSocket`, global `FormData`, global `atob`). `scripts/bundle.mjs` keeps a `browserIgnoreModules` hook, currently an empty array, for the day that changes. Prefer a platform global or a browser-safe dep over reintroducing a Node-only one.

esbuild `define` injects two compile-time constants: `process.env.PKG_VERSION` (read from `package.json`) and `process.env.CLIENT_BUNDLE` (one of `node-cjs`, `browser-cjs`, `browser-esm`). Both are consumed by `StreamChat.getUserAgent()` to produce a bundle-aware UA string. **`tsc`-only code paths do not get this substitution** — these env vars only resolve in the esbuild bundles, so don't gate runtime logic on them in code that callers might import directly via `src/`.

Expand All @@ -56,9 +56,9 @@ This is a single-package SDK with **no monorepo**. The public surface is everyth
- **`ChannelManager.ts`** — channel _lists_. Holds one or more `ChannelPaginator`s (`state.paginators`), keeps them in sync with WS events through an `EventHandlerPipeline` per event type, and arbitrates ownership when a channel matches several lists (`ownershipResolver` / `createPriorityOwnershipResolver`). Replaced the old `channel_manager.ts` (single hand-sorted `state.channels` list with named handler overrides) in v10 — see `v9-to-v10-migration-guide-methods.md`. Filtering and ordering are the paginator's job: `matchesFilter()` runs the filter compiler over `Channel` field resolvers and ordering comes from a comparator compiled from `sort`. The manager is instantiated by the `StreamChat` constructor and lives as long as the client (`client.channelManager`) — it is not configurable through the client options; register lists with `insertPaginator({ paginator, index? })`, detach them with `removePaginator(paginatorOrId)` and set cross-list ownership with `setOwnershipResolver(resolverOrPriorityIds?)`. `setPaginators(paginators)` is the primitive the other two build on — use it (or `clearPaginators()`) for batches, since it publishes one state update instead of one per paginator, and skips the update entirely when the set is unchanged. Registration and loaded data have different owners: `disconnectUser` calls `resetPaginatorStates()`, which discards each list's channels (they belong to the user going away) while leaving the lists themselves registered, since which lists exist is the integrator's configuration. Event handling stays customizable: `ChannelManagerOptions.eventHandlers` replaces the default map wholesale at construction (start from `getDefaultHandlers()` to enrich it instead), and `addEventHandler` / `setEventHandlers` / `removeEventHandlers` adjust the pipelines afterwards — which is the only route for `client.channelManager`, since the client constructs it without options. The exported `ignoreEventsForUnknownChannels` handler, inserted at `index: 0`, is how a list opts out of pulling in channels it has not loaded.
- **`connection.ts` (`StableWSConnection`) + `connection_fallback.ts` (`WSConnectionFallback`)** — realtime transport. Primary WS implementation does its own 25s ping / 35s health-check loop and reconnects on close/error/offline events; the fallback long-polls over HTTP. The client picks between them based on first-connect outcome; both emit `connection.changed` / `transport.changed` events into the client's local event bus.
- **`store.ts` — `StateStore`.** Reactive primitive (see "State and subscription patterns" below).
- **`signing.ts` — webhook + token helpers.** Server-side primitives `verifyAndParseWebhook`, `parseSqs`, `parseSns`, `verifySignature` (recent CHA-3071 added compressed-payload support). These are re-exported through `client.ts`. **The HMAC is always computed over the uncompressed JSON bytes** — gzip detection uses the `1f 8b` magic bytes, not headers, so the same handler works whether your platform middleware auto-decompressed or not. `CheckSignature` is deprecated in favor of `verifySignature` purely to fix parameter order; new code should use `verifySignature(body, signature, secret)`.
- **`signing.ts` — one function, `UserFromToken`.** Decodes a JWT payload with the global `atob` and returns `user_id`. Everything else this module used to hold was server-side (JWT minting via `jsonwebtoken`, webhook/SQS/SNS verification via `crypto` + `zlib`) and was removed along with those deps — see `v9-to-v10-migration-guide-server-side.md`. Do not reintroduce secret-holding or HMAC code here; that surface lives in `@stream-io/node-sdk`.
- **`middleware.ts`** — `MiddlewareExecutor` (see "Middleware pipelines" below). Used by composer pipelines, not by client request lifecycle.
- **`token_manager.ts`** — handles static tokens and async token providers. Tracks a `loadTokenPromise` so concurrent calls await the same fetch. Server-side clients (constructed with a `secret`) sign their own JWTs locally via `JWTServerToken` / `JWTUserToken`.
- **`token_manager.ts`** — handles static tokens and async token providers. Tracks a `loadTokenPromise` so concurrent calls await the same fetch. The constructor takes no arguments: there is no `secret` and no local JWT signing — every token comes from the caller (a string or a `TokenProvider`). Anonymous users may have no token at all; anyone else without one now fails at `getToken()` rather than at `setTokenOrProvider()`.
- **`events.ts` — `EVENT_MAP`.** Single source of truth for known event types (used by `EventTypes` in `types.ts`). Adding a new event type requires an entry here. Note the "local events" section: `channels.queried`, `connection.changed`, `transport.changed`, `capabilities.changed`, `live_location_sharing.*` are dispatched client-side only and never come over the wire.
- **`insights.ts` — `InsightMetrics` + `postInsights`.** WS-health telemetry sent to `https://chat-insights.getstream.io`. This is internal; do not call from end-user code paths. The fields captured by `buildWsBaseInsight` include token and connection metadata — treat changes here as security-sensitive.
- **`uploadManager.ts` / `LiveLocationManager.ts` / `CooldownTimer.ts`** — feature controllers, each owns its own `StateStore` slice.
Expand Down Expand Up @@ -171,7 +171,7 @@ Release branches (`.releaserc.json`):
- `yarn types` passes.
- `yarn test` green.
- If you touched `src/index.ts` or any re-exported type, you've considered the public-API/semver impact.
- If you added a dependency: it doesn't need to run lifecycle scripts (or is added to `dependenciesMeta`), and Node-only deps are listed in `package.json#browser`.
- If you added a dependency: it doesn't need to run lifecycle scripts (or is added to `dependenciesMeta`), and it is not Node-only — the SDK is client-side only and carries no `package.json#browser` shim list anymore, so a Node-only import would break browser/RN bundles outright.
- If you added a new event type, it's registered in `src/events.ts#EVENT_MAP` (otherwise `EventTypes` won't include it).
- If you extended composer behavior, you inserted middleware rather than forking `MessageComposerMiddlewareExecutor`.
- If you added long-lived subscriptions on a manager, `registerSubscriptions` is idempotent and `unregisterSubscriptions` calls `super`.
57 changes: 39 additions & 18 deletions docs/fileUpload.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,62 @@
# File Upload

Stream JS client supports uploading files in both browser and Node.js environment.
`stream-chat` uploads files from the browser and from React Native. The upload
methods accept `string | File`:

- **Browser** — a `File` or `Blob` (typically from an `<input type="file">`).
- **React Native** — a local URI `string`, in which case you must also pass
`contentType`, since there is nothing to infer the MIME type from.

## Token

You can get your API key and API secret in [Stream Dashboard](https://getsream.io/dashboard/).
User token can be generated using your API Secret and any random User ID using [Stream Token Generator](https://getstream.io/chat/docs/javascript/token_generator/).
You can get your API key in the [Stream Dashboard](https://getstream.io/dashboard/).
A user token can be generated for testing with the
[Stream Token Generator](https://getstream.io/chat/docs/javascript/token_generator/);
in production, mint it on your backend with
[`@stream-io/node-sdk`](https://github.com/GetStream/stream-node) and never ship
the API secret to a client.

```js
const apiKey = 'swde2zgm3549';
const apiSecret = 'YOUR_SUPER_SECRET_TOKEN';
const userId = 'dawn-union-6';
const userToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZGF3bi11bmlvbi02In0.mpf8pgxn5r02EqsChMaw6SdCFCyBBl7VJhyleTqEwho';
```

## Node.js
## Node.js — not supported

In order to upload a file, you first need to create an instance of stream client, and a channel to send the files to it.
There is no Node upload path. `stream-chat` v9 accepted a `Buffer` or a
readable stream because it bundled the `form-data` package; v10 dropped that
dependency in favor of the platform's global `FormData`, so `Buffer` and
stream sources are gone and the `Blob` branch only runs where `window` exists.

```js
const fs = require('fs');
const { StreamChat } = require('stream-chat');
Upload from your backend with `@stream-io/node-sdk` instead:

const user = { id: 'user_id' };
const apiKey = 'swde2zgm3549'; // use your app key
const apiSecret = 'YOUR_SUPER_SECRET_TOKEN'; // use your app secret
const client = StreamChat.getInstance(apiKey, apiSecret);
```js
const { readFile } = require('node:fs/promises');
const { File } = require('node:buffer');
const { StreamClient } = require('@stream-io/node-sdk');

const channel = client.channel('messaging', 'channel_id', { created_by: user });
await channel.create(); // if channel does not exist yet
const client = new StreamClient(process.env.STREAM_KEY, process.env.STREAM_SECRET);
const buffer = await readFile('./helloworld.txt');

const file = fs.createReadStream('./helloworld.txt');
const response = await channel.sendFile(file, 'helloworld.txt', 'text/plain', user);
const response = await client.uploadFile({
file: new File([buffer], 'helloworld.txt', { type: 'text/plain' }),
user: { id: 'user_id' },
});
console.log('file url: ', response.file);
```

## React Native

```js
const response = await channel.sendFile(
localUri, // e.g. 'file:///.../IMG_0001.HEIC' from the image picker
'IMG_0001.HEIC',
'image/heic', // required — pass the MIME type explicitly
);
```

## Browser

```html
Expand Down Expand Up @@ -76,7 +97,7 @@ console.log('file url: ', response.file);

Channel uploads use Axios under the hood. Both **`channel.sendFile`** and **`channel.sendImage`** accept an optional **fifth argument** `axiosRequestConfig` (`AxiosRequestConfig` from axios). The same optional argument exists on **`client.uploadFile`** and **`client.uploadImage`**.

The client merges your config **after** its upload defaults (`timeout: 0`, large `maxContentLength` / `maxBodyLength`, and multipart headers from the form data). Any property you set can override or extend those defaults.
The client merges your config **after** its upload defaults (`timeout: 0`, large `maxContentLength` / `maxBodyLength`). Any property you set can override or extend those defaults. Multipart headers — including the boundary — are set by axios from the `FormData` body; the SDK no longer computes them itself (v9 took them from `form-data`'s `getHeaders()`).

Typical uses:

Expand Down
Loading