Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c3fb2a4
Initial commit
arnautov-anton May 7, 2026
b4f37ad
Prepare generated files
arnautov-anton May 7, 2026
fba6b46
Generate filter condition types
arnautov-anton May 12, 2026
d4dd66d
ChatApi/ChannelApi integration \w type changes
arnautov-anton May 14, 2026
d4cfa9f
More file adjustments and reverts
arnautov-anton Jun 2, 2026
0d57a5d
Adjust tests
arnautov-anton Jun 10, 2026
3abbe06
Serverside removal phase 1
arnautov-anton Jun 10, 2026
f8b79f6
Re-generate from new spec
arnautov-anton Jun 15, 2026
789e645
More BE API removal, post-merge fixes and JSDoc cleanup
arnautov-anton Jun 16, 2026
93c92af
tmp1
arnautov-anton Jun 23, 2026
90191d1
Fix tests
arnautov-anton Jun 25, 2026
25799e5
Adjust JSDoc, define rules, add linting
arnautov-anton Jun 26, 2026
c5aadaa
Replace logging system with @stream-io/logger
arnautov-anton Jun 26, 2026
a44ecfe
Re-generate from new spec, adjust ESLint config
arnautov-anton Jun 29, 2026
b900162
Get rid of client construction overloads
arnautov-anton Jun 29, 2026
e4fe8eb
Drop serverside instantiation options
arnautov-anton Jun 30, 2026
9062210
Further cleanup
arnautov-anton Jul 1, 2026
f0857b9
Tmp - migration guides
arnautov-anton Jul 2, 2026
6aa769c
Cleanup and adjustments
arnautov-anton Jul 3, 2026
eceb60d
Post-merge fixes
arnautov-anton Jul 3, 2026
99de10c
More cleanup and type changes
arnautov-anton Jul 7, 2026
68757cb
Post-merge fixes
arnautov-anton Jul 9, 2026
2022a3b
Some more post-merge fixes
arnautov-anton Jul 9, 2026
e7bfec7
Adjust guides (lint+type renames)
arnautov-anton Jul 9, 2026
60d384b
Some more cleanup
arnautov-anton Jul 9, 2026
d2231ab
Adjust guides
arnautov-anton Jul 10, 2026
5408894
Lint fixes
arnautov-anton Jul 14, 2026
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
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ $ yarn run test-unit

We use [ESLint](https://eslint.org/) for linting and [Prettier](https://prettier.io/) for code formatting. We enforce it during the build process. If your IDE has integration with these tools, it's recommended to set them up.

## JSDoc

See [`JSDOC.md`](./JSDOC.md) for the canonical JSDoc format used in this repository. The format is enforced by `eslint-plugin-jsdoc`.

## Commit message convention

Since we're autogenerating our [CHANGELOG](./CHANGELOG.md), we need to follow a specific commit message convention.
Expand Down
66 changes: 66 additions & 0 deletions JSDOC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# JSDoc style guide

This repository uses TSDoc-flavored JSDoc. The TypeScript signature is the source of truth for parameter and return **types** and for **optionality** — JSDoc only carries human-readable descriptions and the few semantic tags listed below.

The format is enforced by `eslint-plugin-jsdoc`. Run `yarn lint` to validate locally.

## Canonical block

````ts
/**
* One-sentence summary in sentence case, ending with a period.
*
* Optional longer prose paragraph. Wrap at ~100 columns.
*
* @param name - Description in sentence case, ending with a period.
* @param options - Send options.
* @param options.skip_enrich_url - Skip URL enrichment for this message.
* @returns Description of the return value.
* @throws When the channel is frozen.
* @deprecated Use {@link newName} instead.
* @example
* ```ts
* client.connectUser({ id: 'foo' }, token);
* ```
*/
````

## Rules

- **No `{Type}` annotations on `@param` / `@returns`.** TypeScript already provides them. Enforced by `jsdoc/no-types`.
- **No bracketed-optional syntax** (`@param [name]`). TypeScript marks optionality via `?` or default values. Enforced by `jsdoc/check-param-names`.
- **Use `@returns`, not `@return`.** Enforced by `jsdoc/check-tag-names`.
- **Drop legacy tags**: `@method`, `@memberof`, `@class`, `@type` — TypeScript provides these.
- **Allowed tags**: `@param`, `@returns`, `@throws`, `@example`, `@default`, `@deprecated`, `@see`, `@internal`, `@private`, `@experimental`, `@remarks`, `@template`, and the inline `{@link}`.
- **Hyphen before description**: `@param name - description`. Enforced by `jsdoc/require-hyphen-before-param-description`.
- **Destructured object params** use dot notation: `@param options.foo - ...`.
- **`@deprecated`** must point at the replacement: `@deprecated Use {@link newName} instead.`
- **Short single-line form** `/** Foo. */` is allowed only when there are no tags and the description fits on one line.
- **Field-level JSDoc** on interfaces/types may use the single-line form (mirrors `src/gen/models/index.ts`).

## Casing in prose

Apply consistently in JSDoc and `//` comments. Do **not** rewrite identifiers, string literals, or `@example` code blocks.

| Wrong | Right |
| ------------ | ------------ |
| `websocket` | `WebSocket` |
| `sdk` | `SDK` |
| `api` | `API` |
| `url` | `URL` |
| `json` | `JSON` |
| `http(s)` | `HTTP(S)` |
| `jwt` | `JWT` |
| `id` (prose) | `ID` |
| `javascript` | `JavaScript` |
| `typescript` | `TypeScript` |

## Reusing field descriptions

Hand-written types in `src/types.ts` and elsewhere often share field names with the OpenAPI-generated types in `src/gen/models/index.ts` (`cid`, `created_at`, `channel_id`, `team`, `duration`, etc.). When documenting such a field, reuse the wording from the generated model for consistency.

## When in doubt

- Cross-check that `@param` names match the actual parameter names.
- Add `@returns` iff the function returns something other than `void` / `Promise<void>`.
- Keep existing wording verbatim except to fix grammar, typos, casing, or factual mismatches with the signature.
39 changes: 37 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import unusedImports from 'eslint-plugin-unused-imports';
import jsdoc from 'eslint-plugin-jsdoc';

import importPlugin from 'eslint-plugin-import';

Expand All @@ -18,6 +20,7 @@ export default tseslint.config(
},
plugins: {
import: importPlugin,
'unused-imports': unusedImports,
},
settings: {
react: {
Expand Down Expand Up @@ -71,9 +74,18 @@ export default tseslint.config(
},
],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{ ignoreRestSiblings: false, caughtErrors: 'none' },
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
ignoreRestSiblings: false,
caughtErrors: 'none',
},
],
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-wrapper-object-types': 'error',
Expand All @@ -83,6 +95,29 @@ export default tseslint.config(
'@typescript-eslint/no-require-imports': 'off', // TODO: remove this rule once all files are .mjs (and require is not used)
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
{
ignores: ['src/gen/**'],
files: ['src/**/*.{js,ts}'],
plugins: {
jsdoc,
},
rules: {
'jsdoc/no-types': 'error',
'jsdoc/check-param-names': ['error', { checkDestructured: false }],
'jsdoc/check-tag-names': [
'error',
{ definedTags: ['internal', 'experimental', 'remarks'] },
],
'jsdoc/require-param-description': 'warn',
'jsdoc/require-returns-description': 'warn',
'jsdoc/require-hyphen-before-param-description': ['warn', 'always'],
'jsdoc/tag-lines': ['error', 'any', { startLines: 1 }],
'jsdoc/no-multi-asterisks': 'error',
'jsdoc/empty-tags': 'error',
'jsdoc/no-bad-blocks': 'error',
},
},
);
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"/src"
],
"dependencies": {
"@stream-io/logger": "^2.0.0",
"@types/jsonwebtoken": "^9.0.8",
"@types/ws": "^8.18.1",
"axios": "^1.16.1",
Expand All @@ -75,6 +76,8 @@
"esbuild": "^0.28.0",
"eslint": "^9.39.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsdoc": "^63.0.7",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.6.0",
"husky": "^9.1.7",
"lint-staged": "^17.0.5",
Expand All @@ -90,7 +93,7 @@
"start": "concurrently 'tsc --watch' './scripts/bundle.mjs --watch'",
"types": "tsc --noEmit",
"lint": "yarn run prettier && yarn run eslint",
"lint-fix": "yarn run prettier-fix && yarn run eslint-fix",
"lint-fix": "yarn run eslint-fix; yarn run prettier-fix",
"prettier": "prettier '**/*.{json,js,mjs,ts,yml,md}' --check",
"prettier-fix": "yarn run prettier --write",
"eslint": "eslint --max-warnings 0",
Expand All @@ -104,7 +107,8 @@
"fix-staged": "lint-staged --config .lintstagedrc.fix.json --concurrent 1",
"semantic-release": "semantic-release",
"postinstall": "node -e \"require('fs').existsSync('scripts/install-husky.mjs') && import('./scripts/install-husky.mjs')\"",
"prepare": "yarn run build"
"prepare": "yarn run build",
"generate-client": "./scripts/generate-client.sh"
},
"engines": {
"node": ">=18"
Expand Down
11 changes: 11 additions & 0 deletions scripts/generate-client.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
set -euo pipefail

OUTPUT_DIR="../stream-chat-js/src/gen"
CHAT_DIR="../chat"

rm -rf $OUTPUT_DIR

( cd $CHAT_DIR ; make openapi ; make -C projects/chat-manager build; build/chat-manager openapi generate-client --language ts --spec releases/v2/chat-clientside-api.yaml --output $OUTPUT_DIR )

yarn lint-fix
120 changes: 120 additions & 0 deletions scripts/generate-filter-types.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { parseArgs, type ParseArgsOptionsConfig } from 'node:util';
import { parse } from 'yaml';

const options = {
spec: {
type: 'string',
short: 's',
},
out: {
type: 'string',
short: 'o',
},
} satisfies ParseArgsOptionsConfig;

type OpenAPISpecification = {
components: {
schemas: {
[key: string]: {
properties?: Partial<
Record<
'filter_conditions' | string,
Partial<
Record<
'x-stream-filter-fields' | string,
Record<
string,
{
operators: string[];
type: string;
}
>
>
>
>
>;
};
};
};
};

const { values } = parseArgs({
args: process.argv,
options,
allowPositionals: true,
tokens: false,
});

const specPath = values.spec;
const outputPath = values.out;

if (!specPath || !outputPath) {
console.error(
'Usage: node generate-filter-types.mts -s <path-to-openapi-specification> -o <output-file>',
);
process.exit(1);
}

const spec = parse(readFileSync(specPath, 'utf8')) as OpenAPISpecification;
const schemas = spec.components?.schemas;

if (!schemas) {
console.error('No components.schemas found in the specification');
process.exit(1);
}

const lines = [];

const typeMapping = {
string: 'string',
number: 'number',
boolean: 'boolean',
date: 'Date',
};

const snakeToCamelCase = (snakeCaseString: string) =>
snakeCaseString
.split('_')
.map((wordSegment) => wordSegment.slice(0, 1).toUpperCase() + wordSegment.slice(1))
.join('');

for (const [schemaName, schema] of Object.entries(schemas)) {
if (!schema.properties) {
console.log(schemaName, 'missing');
continue;
}

for (const [propertyName, propertyDef] of Object.entries(schema.properties)) {
if (!propertyDef?.['x-stream-filter-fields']) continue;

const filterFields = propertyDef['x-stream-filter-fields'];

let typeName = `${schemaName}${snakeToCamelCase(propertyName)}`;

const fieldEntries = Object.entries(filterFields).map(
([fieldName, fieldDefinition]) => {
// TODO: add support for such properties later on (custom/nested filters)
if (fieldDefinition.type === 'object' || fieldName.startsWith('_')) {
return '';
}

const operators = fieldDefinition.operators.length
? fieldDefinition.operators.map((operator) => `"${operator}"`).join(' | ')
: 'never';
return ` "${fieldName}": { type: ${typeMapping[fieldDefinition.type as keyof typeof typeMapping] ?? `"${fieldDefinition.type}"`}; operators: ${operators} };`;
},
);

lines.push(`export type ${typeName} = {`);
lines.push(...fieldEntries);
lines.push(`};\n`);
}
}

if (lines.length > 0) {
writeFileSync(outputPath, '\n' + lines.join('\n') + '\n');
console.log(`Appended ${lines.length} lines to ${outputPath}`);
} else {
console.log('No filter types found');
}
2 changes: 1 addition & 1 deletion src/CooldownTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class CooldownTimer extends WithSubscriptions {

private getOwnUserId() {
const client = this.channel.getClient();
return client.userID ?? client.user?.id;
return client.userId ?? client.user?.id;
}

private findOwnLatestMessageDate({
Expand Down
Loading