From e0cff7dfaffdb6348889bc0689cb13c5aea6073b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 15 Apr 2026 10:17:52 +0530 Subject: [PATCH 1/5] chore: update Web SDK to 24.2.0 --- .github/workflows/publish.yml | 6 +- CHANGELOG.md | 8 + README.md | 4 +- package-lock.json | 576 +++++++++++++++++++------- package.json | 15 +- rollup.config.js => rollup.config.mjs | 4 +- src/client.ts | 4 +- src/enums/o-auth-provider.ts | 1 + src/models.ts | 4 + src/services/account.ts | 8 +- src/services/databases.ts | 4 +- src/services/tables-db.ts | 4 +- 12 files changed, 473 insertions(+), 165 deletions(-) rename rollup.config.js => rollup.config.mjs (91%) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 93199514..e6ea266e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,11 +19,11 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24.14.1' registry-url: 'https://registry.npmjs.org' - - name: Update npm to latest version for OIDC support - run: npm install -g npm@latest + - name: Pin npm for trusted publishing + run: npm install -g npm@11.10.0 - name: Determine release tag id: release_tag diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c4c5d56..c4b4d163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 24.2.0 + +* Added `x` OAuth provider to `OAuthProvider` enum +* Added `userType` field to `Log` model +* Updated `X-Appwrite-Response-Format` header to `1.9.1` +* Updated TTL description for list caching in Databases and TablesDB +* Updated dev dependencies: Rollup 3→4, related plugin upgrades + ## 24.1.1 * Fixed: Added `files` field to `package.json` to publish only built artifacts to npm diff --git a/README.md b/README.md index 9cdfe004..7764e90b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Appwrite Web SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-web.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.0-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.1-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) @@ -33,7 +33,7 @@ import { Client, Account } from "appwrite"; To install with a CDN (content delivery network) add the following scripts to the bottom of your tag, but before you use any Appwrite services: ```html - + ``` diff --git a/package-lock.json b/package-lock.json index 72593df9..4bca5085 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,24 @@ { "name": "appwrite", - "version": "24.1.1", + "version": "24.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite", - "version": "24.1.1", + "version": "24.2.0", "license": "BSD-3-Clause", "dependencies": { "json-bigint": "1.0.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "25.0.8", - "@rollup/plugin-node-resolve": "15.3.1", - "@rollup/plugin-typescript": "11.1.6", + "@rollup/plugin-commonjs": "29.0.2", + "@rollup/plugin-node-resolve": "16.0.3", + "@rollup/plugin-typescript": "12.3.0", "@types/json-bigint": "1.0.4", "playwright": "1.56.1", - "rollup": "3.29.5", - "serve-handler": "6.1.0", + "rollup": "4.60.1", + "serve-handler": "6.1.7", "tslib": "2.8.1", "typescript": "5.7.3" } @@ -31,21 +31,22 @@ "license": "MIT" }, "node_modules/@rollup/plugin-commonjs": { - "version": "25.0.8", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", - "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", + "integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==", "dev": true, "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", - "glob": "^8.0.3", + "fdir": "^6.2.0", "is-reference": "1.2.1", - "magic-string": "^0.30.3" + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0 || 14 >= 14.17" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" @@ -57,9 +58,9 @@ } }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", - "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", "dev": true, "license": "MIT", "dependencies": { @@ -82,9 +83,9 @@ } }, "node_modules/@rollup/plugin-typescript": { - "version": "11.1.6", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", - "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", + "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", "dev": true, "license": "MIT", "dependencies": { @@ -131,6 +132,356 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -169,13 +520,14 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/bytes": { @@ -229,23 +581,24 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "punycode": "^1.3.2" + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -271,27 +624,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -305,25 +637,6 @@ "node": ">= 0.4" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -400,26 +713,16 @@ } }, "node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" + "node": "*" } }, "node_modules/path-is-inside": { @@ -437,9 +740,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", - "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", "dev": true, "license": "MIT" }, @@ -488,13 +791,6 @@ "node": ">=18" } }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", @@ -527,63 +823,66 @@ } }, "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, "node_modules/serve-handler": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.0.tgz", - "integrity": "sha512-63N075Tn3PsFYcu0NVV7tb367UbiW3gnC+/50ohL4oqOhAG6bmbaWqiRcXQgbzqc0ALBjSAzg7VTfa0Qw4E3hA==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", "dev": true, "license": "MIT", "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", - "fast-url-parser": "1.1.3", "mime-types": "2.1.18", - "minimatch": "3.0.4", + "minimatch": "3.1.5", "path-is-inside": "1.0.2", - "path-to-regexp": "2.2.1", + "path-to-regexp": "3.3.0", "range-parser": "1.2.0" } }, - "node_modules/serve-handler/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/serve-handler/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -617,13 +916,6 @@ "engines": { "node": ">=14.17" } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" } } } diff --git a/package.json b/package.json index 37964d90..d240cf02 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "appwrite", "homepage": "https://appwrite.io/support", "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", - "version": "24.1.1", + "version": "24.2.0", "license": "BSD-3-Clause", "main": "dist/cjs/sdk.js", "exports": { @@ -28,17 +28,20 @@ "build:types": "tsc --declaration --emitDeclarationOnly --outDir types", "build:libs": "rollup -c" }, + "engines": { + "node": ">=18.0.0" + }, "dependencies": { "json-bigint": "1.0.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "25.0.8", - "@rollup/plugin-node-resolve": "15.3.1", - "@rollup/plugin-typescript": "11.1.6", + "@rollup/plugin-commonjs": "29.0.2", + "@rollup/plugin-node-resolve": "16.0.3", + "@rollup/plugin-typescript": "12.3.0", "@types/json-bigint": "1.0.4", "playwright": "1.56.1", - "rollup": "3.29.5", - "serve-handler": "6.1.0", + "rollup": "4.60.1", + "serve-handler": "6.1.7", "tslib": "2.8.1", "typescript": "5.7.3" }, diff --git a/rollup.config.js b/rollup.config.mjs similarity index 91% rename from rollup.config.js rename to rollup.config.mjs index 72a093a3..eabf435c 100644 --- a/rollup.config.js +++ b/rollup.config.mjs @@ -10,7 +10,7 @@ export default [ { input: "src/index.ts", external, - plugins: [typescript()], + plugins: [typescript({ outDir: "dist" })], output: [ { format: "cjs", @@ -30,7 +30,7 @@ export default [ plugins: [ resolve({ browser: true }), commonjs(), - typescript() + typescript({ outDir: "dist" }) ], output: [ { diff --git a/src/client.ts b/src/client.ts index 436a4f71..ca15ae48 100644 --- a/src/client.ts +++ b/src/client.ts @@ -398,8 +398,8 @@ class Client { 'x-sdk-name': 'Web', 'x-sdk-platform': 'client', 'x-sdk-language': 'web', - 'x-sdk-version': '24.1.1', - 'X-Appwrite-Response-Format': '1.9.0', + 'x-sdk-version': '24.2.0', + 'X-Appwrite-Response-Format': '1.9.1', }; /** diff --git a/src/enums/o-auth-provider.ts b/src/enums/o-auth-provider.ts index 3382e3bf..efc44844 100644 --- a/src/enums/o-auth-provider.ts +++ b/src/enums/o-auth-provider.ts @@ -33,6 +33,7 @@ export enum OAuthProvider { TradeshiftBox = 'tradeshiftBox', Twitch = 'twitch', Wordpress = 'wordpress', + X = 'x', Yahoo = 'yahoo', Yammer = 'yammer', Yandex = 'yandex', diff --git a/src/models.ts b/src/models.ts index 82c2e70d..ef424e14 100644 --- a/src/models.ts +++ b/src/models.ts @@ -334,6 +334,10 @@ export namespace Models { * API mode when event triggered. */ mode: string; + /** + * User type who triggered the audit log. Possible values: user, admin, guest, keyProject, keyAccount, keyOrganization. + */ + userType: string; /** * IP session in use when the session was created. */ diff --git a/src/services/account.ts b/src/services/account.ts index 13dd2f5a..d15c2c71 100644 --- a/src/services/account.ts +++ b/src/services/account.ts @@ -1863,7 +1863,7 @@ export class Account { * A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits). * * - * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. * @param {string} params.success - URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string} params.failure - URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string[]} params.scopes - A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. @@ -1879,7 +1879,7 @@ export class Account { * A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits). * * - * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. * @param {string} success - URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string} failure - URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string[]} scopes - A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. @@ -2616,7 +2616,7 @@ export class Account { * * A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits). * - * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} params.provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. * @param {string} params.success - URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string} params.failure - URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string[]} params.scopes - A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. @@ -2631,7 +2631,7 @@ export class Account { * * A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits). * - * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom. + * @param {OAuthProvider} provider - OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. * @param {string} success - URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string} failure - URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param {string[]} scopes - A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. diff --git a/src/services/databases.ts b/src/services/databases.ts index d7e349de..51a58d81 100644 --- a/src/services/databases.ts +++ b/src/services/databases.ts @@ -351,7 +351,7 @@ export class Databases { * @param {string[]} params.queries - Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. * @param {string} params.transactionId - Transaction ID to read uncommitted changes within the transaction. * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. - * @param {number} params.ttl - TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours). + * @param {number} params.ttl - TTL (seconds) for caching list responses. Responses are stored in an in-memory key-value cache, keyed per project, collection, schema version (attributes and indexes), caller authorization roles, and the exact query — so users with different permissions never share cached entries. Schema changes invalidate cached entries automatically; document writes do not, so choose a TTL you are comfortable serving as stale data. Set to 0 to disable caching. Must be between 0 and 86400 (24 hours). * @throws {AppwriteException} * @returns {Promise>} * @deprecated This API has been deprecated since 1.8.0. Please use `TablesDB.listRows` instead. @@ -365,7 +365,7 @@ export class Databases { * @param {string[]} queries - Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. * @param {string} transactionId - Transaction ID to read uncommitted changes within the transaction. * @param {boolean} total - When set to false, the total count returned will be 0 and will not be calculated. - * @param {number} ttl - TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours). + * @param {number} ttl - TTL (seconds) for caching list responses. Responses are stored in an in-memory key-value cache, keyed per project, collection, schema version (attributes and indexes), caller authorization roles, and the exact query — so users with different permissions never share cached entries. Schema changes invalidate cached entries automatically; document writes do not, so choose a TTL you are comfortable serving as stale data. Set to 0 to disable caching. Must be between 0 and 86400 (24 hours). * @throws {AppwriteException} * @returns {Promise>} * @deprecated Use the object parameter style method for a better developer experience. diff --git a/src/services/tables-db.ts b/src/services/tables-db.ts index 8f709c86..fa671f14 100644 --- a/src/services/tables-db.ts +++ b/src/services/tables-db.ts @@ -351,7 +351,7 @@ export class TablesDB { * @param {string[]} params.queries - Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. * @param {string} params.transactionId - Transaction ID to read uncommitted changes within the transaction. * @param {boolean} params.total - When set to false, the total count returned will be 0 and will not be calculated. - * @param {number} params.ttl - TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours). + * @param {number} params.ttl - TTL (seconds) for caching list responses. Responses are stored in an in-memory key-value cache, keyed per project, table, schema version (columns and indexes), caller authorization roles, and the exact query — so users with different permissions never share cached entries. Schema changes invalidate cached entries automatically; row writes do not, so choose a TTL you are comfortable serving as stale data. Set to 0 to disable caching. Must be between 0 and 86400 (24 hours). * @throws {AppwriteException} * @returns {Promise>} */ @@ -364,7 +364,7 @@ export class TablesDB { * @param {string[]} queries - Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. * @param {string} transactionId - Transaction ID to read uncommitted changes within the transaction. * @param {boolean} total - When set to false, the total count returned will be 0 and will not be calculated. - * @param {number} ttl - TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours). + * @param {number} ttl - TTL (seconds) for caching list responses. Responses are stored in an in-memory key-value cache, keyed per project, table, schema version (columns and indexes), caller authorization roles, and the exact query — so users with different permissions never share cached entries. Schema changes invalidate cached entries automatically; row writes do not, so choose a TTL you are comfortable serving as stale data. Set to 0 to disable caching. Must be between 0 and 86400 (24 hours). * @throws {AppwriteException} * @returns {Promise>} * @deprecated Use the object parameter style method for a better developer experience. From 1469c61305742538deb6b8fd7ca70ae4ba66d7ca Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Apr 2026 08:19:59 +0000 Subject: [PATCH 2/5] chore: update Web SDK to 24.2.0 --- README.md | 4 ++-- src/client.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7764e90b..4b6643a9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Appwrite Web SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-web.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.1-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.0-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK is compatible with Appwrite server version 1.9.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-web/releases).** +**This SDK is compatible with Appwrite server version latest. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-web/releases).** Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Web SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) diff --git a/src/client.ts b/src/client.ts index ca15ae48..95f43596 100644 --- a/src/client.ts +++ b/src/client.ts @@ -399,7 +399,7 @@ class Client { 'x-sdk-platform': 'client', 'x-sdk-language': 'web', 'x-sdk-version': '24.2.0', - 'X-Appwrite-Response-Format': '1.9.1', + 'X-Appwrite-Response-Format': '1.9.0', }; /** From 536342a768316842dad5ff3e742fd235b2913608 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Apr 2026 08:31:54 +0000 Subject: [PATCH 3/5] chore: update Web SDK to 24.3.0 --- CHANGELOG.md | 6 ++ README.md | 4 +- package-lock.json | 4 +- package.json | 2 +- src/client.ts | 182 +++++++++++++++++++++++++-------------- src/services/realtime.ts | 160 ++++++++++++++++++++++++---------- 6 files changed, 241 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b4d163..4942d777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 24.3.0 + +* Added: `Realtime` switched to explicit `subscribe` messages with per-subscription queries +* Fixed: `Realtime` subscription IDs persisted correctly across reconnects +* Updated: README compatibility note now targets `latest` server version + ## 24.2.0 * Added `x` OAuth provider to `OAuthProvider` enum diff --git a/README.md b/README.md index 4b6643a9..e1e2f056 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Appwrite Web SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-web.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.0-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.1-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) @@ -33,7 +33,7 @@ import { Client, Account } from "appwrite"; To install with a CDN (content delivery network) add the following scripts to the bottom of your tag, but before you use any Appwrite services: ```html - + ``` diff --git a/package-lock.json b/package-lock.json index 4bca5085..7041f539 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "appwrite", - "version": "24.2.0", + "version": "24.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite", - "version": "24.2.0", + "version": "24.3.0", "license": "BSD-3-Clause", "dependencies": { "json-bigint": "1.0.0" diff --git a/package.json b/package.json index d240cf02..5b0fabd4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "appwrite", "homepage": "https://appwrite.io/support", "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", - "version": "24.2.0", + "version": "24.3.0", "license": "BSD-3-Clause", "main": "dist/cjs/sdk.js", "exports": { diff --git a/src/client.ts b/src/client.ts index 95f43596..c62d550d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -76,14 +76,30 @@ type RealtimeResponse = { */ type RealtimeRequest = { /** - * Type of the request: 'authentication'. + * Type of the request: 'authentication' or 'subscribe'. */ - type: 'authentication'; + type: 'authentication' | 'subscribe'; /** * Data required for authentication. */ - data: RealtimeRequestAuthenticate; + data: RealtimeRequestAuthenticate | RealtimeRequestSubscribe[]; +} + +type RealtimeRequestSubscribe = { + subscriptionId?: string; + channels: string[]; + queries: string[]; +} + +type RealtimeResponseAction = { + to?: string; + success?: boolean; + subscriptions?: Array<{ + subscriptionId?: string; + channels?: string[]; + queries?: string[]; + }>; } /** @@ -217,11 +233,6 @@ type Realtime = { */ channels: Set; - /** - * Set of query strings the client is subscribed to. - */ - queries: Set; - /** * Map of subscriptions containing channel names and corresponding callback functions. */ @@ -241,6 +252,8 @@ type Realtime = { */ subscriptionIdToSlot: Map; + pendingSubscribeSlots: number[]; + /** * Counter for managing subscriptions. */ @@ -276,12 +289,7 @@ type Realtime = { */ createHeartbeat: () => void; - /** - * Function to clean up resources associated with specified channels. - * - * @param {string[]} channels - List of channel names to clean up. - */ - cleanUp: (channels: string[], queries: string[]) => void; + sendSubscribeMessage: () => void; /** * Function to handle incoming messages from the WebSocket connection. @@ -398,8 +406,8 @@ class Client { 'x-sdk-name': 'Web', 'x-sdk-platform': 'client', 'x-sdk-language': 'web', - 'x-sdk-version': '24.2.0', - 'X-Appwrite-Response-Format': '1.9.0', + 'x-sdk-version': '24.3.0', + 'X-Appwrite-Response-Format': '1.9.1', }; /** @@ -575,10 +583,10 @@ class Client { heartbeat: undefined, url: '', channels: new Set(), - queries: new Set(), subscriptions: new Map(), slotToSubscriptionId: new Map(), subscriptionIdToSlot: new Map(), + pendingSubscribeSlots: [], subscriptionsCounter: 0, reconnect: true, reconnectAttempts: 0, @@ -620,22 +628,8 @@ class Client { } const encodedProject = encodeURIComponent((this.config.project as string) ?? ''); - let queryParams = 'project=' + encodedProject; - - this.realtime.channels.forEach(channel => { - queryParams += '&channels[]=' + encodeURIComponent(channel); - }); - - // Per-subscription queries: channel[slot][]=query so server can route events by subscription - const selectAllQuery = Query.select(['*']).toString(); - this.realtime.subscriptions.forEach((sub, slot) => { - const queries = sub.queries.length > 0 ? sub.queries : [selectAllQuery]; - sub.channels.forEach(channel => { - queries.forEach(query => { - queryParams += '&' + encodeURIComponent(channel) + '[' + slot + '][]=' + encodeURIComponent(query); - }); - }); - }); + // URL carries only the project; channels/queries are sent via subscribe message. + const queryParams = 'project=' + encodedProject; const url = this.config.endpointRealtime + '/realtime?' + queryParams; @@ -679,7 +673,44 @@ class Client { this.realtime.createSocket(); }, timeout); }) + } else if (this.realtime.socket?.readyState === WebSocket.OPEN) { + // URL is unchanged; re-send subscribe message to apply updated queries. + this.realtime.sendSubscribeMessage(); + } + }, + sendSubscribeMessage: () => { + if (!this.realtime.socket || this.realtime.socket.readyState !== WebSocket.OPEN) { + return; + } + + const rows: RealtimeRequestSubscribe[] = []; + this.realtime.pendingSubscribeSlots = []; + + this.realtime.subscriptions.forEach((sub, slot) => { + const queries = sub.queries ?? []; + + const row: RealtimeRequestSubscribe = { + channels: sub.channels, + queries + }; + + const knownSubscriptionId = this.realtime.slotToSubscriptionId.get(slot); + if (knownSubscriptionId) { + row.subscriptionId = knownSubscriptionId; + } + + rows.push(row); + this.realtime.pendingSubscribeSlots.push(slot); + }); + + if (rows.length < 1) { + return; } + + this.realtime.socket.send(JSONbig.stringify({ + type: 'subscribe', + data: rows + })); }, onMessage: (event) => { try { @@ -689,17 +720,29 @@ class Client { case 'connected': { const messageData = message.data; if (messageData?.subscriptions) { - this.realtime.slotToSubscriptionId.clear(); - this.realtime.subscriptionIdToSlot.clear(); for (const [slotStr, subscriptionId] of Object.entries(messageData.subscriptions)) { const slot = Number(slotStr); - if (!isNaN(slot) && typeof subscriptionId === 'string') { - this.realtime.slotToSubscriptionId.set(slot, subscriptionId); - this.realtime.subscriptionIdToSlot.set(subscriptionId, slot); + if (isNaN(slot) || typeof subscriptionId !== 'string') { + continue; } + + const directSlotExists = this.realtime.subscriptions.has(slot); + const shiftedSlot = slot + 1; + const shiftedSlotExists = this.realtime.subscriptions.has(shiftedSlot); + const targetSlot = directSlotExists ? slot : shiftedSlotExists ? shiftedSlot : slot; + + this.realtime.slotToSubscriptionId.set(targetSlot, subscriptionId); + this.realtime.subscriptionIdToSlot.set(subscriptionId, targetSlot); } } + this.realtime.subscriptions.forEach((_sub, slot) => { + const existing = this.realtime.slotToSubscriptionId.get(slot); + if (existing) { + this.realtime.subscriptionIdToSlot.set(existing, slot); + } + }); + let session = this.config.session; if (!session) { const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); @@ -713,6 +756,26 @@ class Client { } })); } + + this.realtime.sendSubscribeMessage(); + break; + } + case 'response': { + const action = message.data as RealtimeResponseAction; + if (action?.to !== 'subscribe' || !Array.isArray(action.subscriptions)) { + break; + } + + action.subscriptions.forEach((subscription, index) => { + const subscriptionId = subscription?.subscriptionId; + const slot = this.realtime.pendingSubscribeSlots[index]; + if (!subscriptionId || slot === undefined) { + return; + } + + this.realtime.slotToSubscriptionId.set(slot, subscriptionId); + this.realtime.subscriptionIdToSlot.set(subscriptionId, slot); + }); break; } case 'event': { @@ -751,31 +814,6 @@ class Client { } catch (e) { console.error(e); } - }, - cleanUp: (channels, queries) => { - this.realtime.channels.forEach(channel => { - if (channels.includes(channel)) { - let found = Array.from(this.realtime.subscriptions).some(([_key, subscription] )=> { - return subscription.channels.includes(channel); - }) - - if (!found) { - this.realtime.channels.delete(channel); - } - } - }) - - this.realtime.queries.forEach(query => { - if (queries.includes(query)) { - let found = Array.from(this.realtime.subscriptions).some(([_key, subscription]) => { - return subscription.queries?.includes(query); - }); - - if (!found) { - this.realtime.queries.delete(query); - } - } - }) } } @@ -835,8 +873,6 @@ class Client { channelStrings.forEach(channel => this.realtime.channels.add(channel)); const queryStrings = (queries ?? []).map(q => typeof q === 'string' ? q : q.toString()); - queryStrings.forEach(query => this.realtime.queries.add(query)); - const counter = this.realtime.subscriptionsCounter++; this.realtime.subscriptions.set(counter, { channels: channelStrings, @@ -847,8 +883,22 @@ class Client { this.realtime.connect(); return () => { + const subscriptionId = this.realtime.slotToSubscriptionId.get(counter); this.realtime.subscriptions.delete(counter); - this.realtime.cleanUp(channelStrings, queryStrings); + this.realtime.slotToSubscriptionId.delete(counter); + if (subscriptionId) { + this.realtime.subscriptionIdToSlot.delete(subscriptionId); + } + // Remove channels that are no longer referenced by any active subscription. + const stillUsed = new Set(); + this.realtime.subscriptions.forEach(sub => { + sub.channels.forEach(channel => stillUsed.add(channel)); + }); + this.realtime.channels.forEach(channel => { + if (!stillUsed.has(channel)) { + this.realtime.channels.delete(channel); + } + }); this.realtime.connect(); } } diff --git a/src/services/realtime.ts b/src/services/realtime.ts index a33b45de..fb70dc16 100644 --- a/src/services/realtime.ts +++ b/src/services/realtime.ts @@ -32,10 +32,24 @@ export type RealtimeResponseConnected = { } export type RealtimeRequest = { - type: 'authentication'; - data: { - session: string; - }; + type: 'authentication' | 'subscribe'; + data: any; +} + +type RealtimeRequestSubscribeRow = { + subscriptionId?: string; + channels: string[]; + queries: string[]; +} + +type RealtimeResponseAction = { + to?: string; + success?: boolean; + subscriptions?: Array<{ + subscriptionId?: string; + channels?: string[]; + queries?: string[]; + }>; } export enum RealtimeCode { @@ -49,6 +63,7 @@ export class Realtime { private readonly TYPE_EVENT = 'event'; private readonly TYPE_PONG = 'pong'; private readonly TYPE_CONNECTED = 'connected'; + private readonly TYPE_RESPONSE = 'response'; private readonly DEBOUNCE_MS = 1; private readonly HEARTBEAT_INTERVAL = 20000; // 20 seconds in milliseconds @@ -63,6 +78,7 @@ export class Realtime { private heartbeatTimer?: number; private subCallDepth = 0; + private pendingSubscribeSlots: number[] = []; private reconnectAttempts = 0; private subscriptionsCounter = 0; private connectionId = 0; @@ -134,39 +150,8 @@ export class Realtime { throw new AppwriteException('Missing project ID'); } - // Collect all unique channels from all slots - const allChannels = new Set(); - for (const subscription of this.activeSubscriptions.values()) { - for (const channel of subscription.channels) { - allChannels.add(channel); - } - } - - let queryParams = `project=${projectId}`; - for (const channel of allChannels) { - queryParams += `&channels[]=${encodeURIComponent(channel)}`; - } - - // Build query string from slots → channels → queries - // Format: channel[slot][]=query - // For each slot, repeat its queries under each channel it subscribes to - // Example: slot 1 → channels [tests, prod], queries [q1, q2] - // Produces: tests[1][]=q1&tests[1][]=q2&prod[1][]=q1&prod[1][]=q2 - const selectAllQuery = Query.select(['*']).toString(); - for (const [slot, subscription] of this.activeSubscriptions) { - // queries is string[] - iterate over each query string - const queries = subscription.queries.length === 0 - ? [selectAllQuery] - : subscription.queries; - - // Repeat this slot's queries under each channel it subscribes to - // Each query is sent as a separate parameter: channel[slot][]=q1&channel[slot][]=q2 - for (const channel of subscription.channels) { - for (const query of queries) { - queryParams += `&${encodeURIComponent(channel)}[${slot}][]=${encodeURIComponent(query)}`; - } - } - } + // URL carries only the project; channels/queries are sent via the subscribe message. + const queryParams = `project=${projectId}`; const endpoint = this.client.config.endpointRealtime !== '' @@ -293,6 +278,44 @@ export class Realtime { return new Promise(resolve => setTimeout(resolve, ms)); } + private getKnownSubscriptionId(slot: number): string | undefined { + return this.slotToSubscriptionId.get(slot); + } + + private sendSubscribeMessage(): void { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + return; + } + + const rows: RealtimeRequestSubscribeRow[] = []; + this.pendingSubscribeSlots = []; + + for (const [slot, subscription] of this.activeSubscriptions) { + const queries = subscription.queries ?? []; + + const row: RealtimeRequestSubscribeRow = { + channels: Array.from(subscription.channels), + queries + }; + const knownSubscriptionId = this.getKnownSubscriptionId(slot); + if (knownSubscriptionId) { + row.subscriptionId = knownSubscriptionId; + } + + rows.push(row); + this.pendingSubscribeSlots.push(slot); + } + + if (rows.length < 1) { + return; + } + + this.socket.send(JSON.stringify({ + type: 'subscribe', + data: rows + })); + } + /** * Convert a channel value to a string * @@ -408,7 +431,11 @@ export class Realtime { await this.sleep(this.DEBOUNCE_MS); if (this.subCallDepth === 1) { - await this.createSocket(); + if (!this.socket || this.socket.readyState > WebSocket.OPEN) { + await this.createSocket(); + } else if (this.socket.readyState === WebSocket.OPEN) { + this.sendSubscribeMessage(); + } } this.subCallDepth--; @@ -421,7 +448,17 @@ export class Realtime { if (subscriptionId) { this.subscriptionIdToSlot.delete(subscriptionId); } - await this.createSocket(); + if (this.activeSubscriptions.size === 0) { + this.reconnect = false; + await this.closeSocket(); + return; + } + + if (!this.socket || this.socket.readyState > WebSocket.OPEN) { + await this.createSocket(); + } else if (this.socket.readyState === WebSocket.OPEN) { + this.sendSubscribeMessage(); + } } }; } @@ -447,6 +484,9 @@ export class Realtime { case this.TYPE_PONG: // Handle pong response if needed break; + case this.TYPE_RESPONSE: + this.handleResponseAction(message); + break; } } @@ -457,16 +497,23 @@ export class Realtime { const messageData = message.data as RealtimeResponseConnected; - // Store subscription ID mappings from backend - // Format: { "0": "sub_a1f9", "1": "sub_b83c", ... } + // Store subscription ID mappings from backend. + // Use direct slot first; if URL mapping is zero-based, try slot+1. if (messageData.subscriptions) { - this.slotToSubscriptionId.clear(); - this.subscriptionIdToSlot.clear(); for (const [slotStr, subscriptionId] of Object.entries(messageData.subscriptions)) { const slot = Number(slotStr); - if (!isNaN(slot)) { - this.slotToSubscriptionId.set(slot, subscriptionId); - this.subscriptionIdToSlot.set(subscriptionId, slot); + if (isNaN(slot)) { + continue; + } + + const directSlotExists = this.activeSubscriptions.has(slot); + const shiftedSlot = slot + 1; + const shiftedSlotExists = this.activeSubscriptions.has(shiftedSlot); + const targetSlot = directSlotExists ? slot : shiftedSlotExists ? shiftedSlot : slot; + + if (typeof subscriptionId === 'string') { + this.slotToSubscriptionId.set(targetSlot, subscriptionId); + this.subscriptionIdToSlot.set(subscriptionId, targetSlot); } } } @@ -489,6 +536,8 @@ export class Realtime { } })); } + + this.sendSubscribeMessage(); } private handleResponseError(message: RealtimeResponse): void { @@ -534,4 +583,23 @@ export class Realtime { } } } + + private handleResponseAction(message: RealtimeResponse): void { + const data = message.data as RealtimeResponseAction | undefined; + if (!data || data.to !== 'subscribe' || !Array.isArray(data.subscriptions)) { + return; + } + + for (let i = 0; i < data.subscriptions.length; i++) { + const subscription = data.subscriptions[i]; + const subscriptionId = subscription?.subscriptionId; + const slot = this.pendingSubscribeSlots[i]; + if (slot === undefined || !subscriptionId) { + continue; + } + + this.slotToSubscriptionId.set(slot, subscriptionId); + this.subscriptionIdToSlot.set(subscriptionId, slot); + } + } } From 880b379fc4f9a4e63494a41bcece844876da13b9 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 20 Apr 2026 14:16:29 +0530 Subject: [PATCH 4/5] chore: restore .github templates and workflows Made-with: Cursor --- .github/ISSUE_TEMPLATE/bug.yaml | 82 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/documentation.yaml | 32 +++++++++ .github/ISSUE_TEMPLATE/feature.yaml | 40 +++++++++++ .github/workflows/autoclose.yml | 11 +++ 4 files changed, 165 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yaml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature.yaml create mode 100644 .github/workflows/autoclose.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 00000000..f97c9416 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,82 @@ +name: "🐛 Bug Report" +description: "Submit a bug report to help us improve" +title: "🐛 Bug Report: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out our bug report form 🙏 + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: "👟 Reproduction steps" + description: "How do you trigger this bug? Please walk us through it step by step." + placeholder: "When I ..." + - type: textarea + id: expected-behavior + validations: + required: true + attributes: + label: "👍 Expected behavior" + description: "What did you think would happen?" + placeholder: "It should ..." + - type: textarea + id: actual-behavior + validations: + required: true + attributes: + label: "👎 Actual Behavior" + description: "What did actually happen? Add screenshots, if applicable." + placeholder: "It actually ..." + - type: dropdown + id: appwrite-version + attributes: + label: "🎲 Appwrite version" + description: "What version of Appwrite are you running?" + options: + - Version 0.10.x + - Version 0.9.x + - Version 0.8.x + - Version 0.7.x + - Version 0.6.x + - Different version (specify in environment) + validations: + required: true + - type: dropdown + id: operating-system + attributes: + label: "💻 Operating system" + description: "What OS is your server / device running on?" + options: + - Linux + - MacOS + - Windows + - Something else + validations: + required: true + - type: textarea + id: enviromnemt + validations: + required: false + attributes: + label: "🧱 Your Environment" + description: "Is your environment customized in any way?" + placeholder: "I use Cloudflare for ..." + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "👀 Have you spent some time to check if this issue has been raised before?" + description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: "🏢 Have you read the Code of Conduct?" + options: + - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation.yaml b/.github/ISSUE_TEMPLATE/documentation.yaml new file mode 100644 index 00000000..c2f829df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yaml @@ -0,0 +1,32 @@ +name: "📚 Documentation" +description: "Report an issue related to documentation" +title: "📚 Documentation: " +labels: [documentation] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to make our documentation better 🙏 + - type: textarea + id: issue-description + validations: + required: true + attributes: + label: "💭 Description" + description: "A clear and concise description of what the issue is." + placeholder: "Documentation should not ..." + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "👀 Have you spent some time to check if this issue has been raised before?" + description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: "🏢 Have you read the Code of Conduct?" + options: + - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 00000000..6181cda7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,40 @@ +name: 🚀 Feature +description: "Submit a proposal for a new feature" +title: "🚀 Feature: " +labels: [feature] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out our feature request form 🙏 + - type: textarea + id: feature-description + validations: + required: true + attributes: + label: "🔖 Feature description" + description: "A clear and concise description of what the feature is." + placeholder: "You should add ..." + - type: textarea + id: pitch + validations: + required: true + attributes: + label: "🎤 Pitch" + description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable." + placeholder: "In my use-case, ..." + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "👀 Have you spent some time to check if this issue has been raised before?" + description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: "🏢 Have you read the Code of Conduct?" + options: + - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" + required: true \ No newline at end of file diff --git a/.github/workflows/autoclose.yml b/.github/workflows/autoclose.yml new file mode 100644 index 00000000..3e2b3cbc --- /dev/null +++ b/.github/workflows/autoclose.yml @@ -0,0 +1,11 @@ +name: Auto-close External Pull Requests + +on: + pull_request_target: + types: [opened, reopened] + +jobs: + auto_close: + uses: appwrite/.github/.github/workflows/autoclose.yml@main + secrets: + GH_AUTO_CLOSE_PR_TOKEN: ${{ secrets.GH_AUTO_CLOSE_PR_TOKEN }} From 2da614054d331d03e67867ac623018869ab35c6e Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 Apr 2026 04:39:49 +0000 Subject: [PATCH 5/5] chore: update Web SDK to 25.0.0 --- CHANGELOG.md | 8 +- README.md | 6 +- package-lock.json | 4 +- package.json | 2 +- src/client.ts | 173 +++++++--------------- src/models.ts | 4 + src/services/realtime.ts | 302 +++++++++++++++++++++------------------ 7 files changed, 233 insertions(+), 266 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4942d777..7492c403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Change Log -## 24.3.0 +## 25.0.0 -* Added: `Realtime` switched to explicit `subscribe` messages with per-subscription queries -* Fixed: `Realtime` subscription IDs persisted correctly across reconnects -* Updated: README compatibility note now targets `latest` server version +* Breaking: Added `unsubscribe()`, `update()`, and `close()` for Realtime subscription lifecycle. +* Added: Added `userPhone` to the `Membership` model. +* Updated: Updated `X-Appwrite-Response-Format` header to `1.9.2`. ## 24.2.0 diff --git a/README.md b/README.md index e1e2f056..6ed4881e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Appwrite Web SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-web.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-1.9.1-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.9.2-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK is compatible with Appwrite server version latest. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-web/releases).** +**This SDK is compatible with Appwrite server version 1.9.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-web/releases).** Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Web SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) @@ -33,7 +33,7 @@ import { Client, Account } from "appwrite"; To install with a CDN (content delivery network) add the following scripts to the bottom of your tag, but before you use any Appwrite services: ```html - + ``` diff --git a/package-lock.json b/package-lock.json index 7041f539..fc146255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "appwrite", - "version": "24.3.0", + "version": "25.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite", - "version": "24.3.0", + "version": "25.0.0", "license": "BSD-3-Clause", "dependencies": { "json-bigint": "1.0.0" diff --git a/package.json b/package.json index 5b0fabd4..feea2e8e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "appwrite", "homepage": "https://appwrite.io/support", "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", - "version": "24.3.0", + "version": "25.0.0", "license": "BSD-3-Clause", "main": "dist/cjs/sdk.js", "exports": { diff --git a/src/client.ts b/src/client.ts index c62d550d..6bd679d2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,7 @@ import { Models } from './models'; import { Channel, ActionableChannel, ResolvedChannel } from './channel'; import { Query } from './query'; +import { ID } from './id'; import JSONbigModule from 'json-bigint'; const JSONbigParser = JSONbigModule({ storeAsString: false }); const JSONbigSerializer = JSONbigModule({ useNativeBigInt: true }); @@ -37,7 +38,7 @@ function reviver(_key: string, value: any): any { return value; } -const JSONbig = { +export const JSONbig = { parse: (text: string) => JSONbigParser.parse(text, reviver), stringify: JSONbigSerializer.stringify }; @@ -87,21 +88,11 @@ type RealtimeRequest = { } type RealtimeRequestSubscribe = { - subscriptionId?: string; + subscriptionId: string; channels: string[]; queries: string[]; } -type RealtimeResponseAction = { - to?: string; - success?: boolean; - subscriptions?: Array<{ - subscriptionId?: string; - channels?: string[]; - queries?: string[]; - }>; -} - /** * Realtime event response structure with generic payload type. */ @@ -160,11 +151,6 @@ type RealtimeResponseConnected = { * User object representing the connected user (optional). */ user?: object; - - /** - * Map slot index -> subscription ID from backend (optional). - */ - subscriptions?: Record; } /** @@ -234,30 +220,18 @@ type Realtime = { channels: Set; /** - * Map of subscriptions containing channel names and corresponding callback functions. + * Map of subscriptions keyed by client-generated subscriptionId. */ - subscriptions: Map) => void }>; /** - * Map slot index -> subscription ID (from backend, set on 'connected'). - */ - slotToSubscriptionId: Map; - - /** - * Map subscription ID -> slot index (for O(1) event dispatch). - */ - subscriptionIdToSlot: Map; - - pendingSubscribeSlots: number[]; - - /** - * Counter for managing subscriptions. + * Pending subscribe rows keyed by subscriptionId. Flushed and cleared on each send. */ - subscriptionsCounter: number; + pendingSubscribes: Map; /** * Boolean indicating whether automatic reconnection is enabled. @@ -289,7 +263,7 @@ type Realtime = { */ createHeartbeat: () => void; - sendSubscribeMessage: () => void; + sendPendingSubscribes: () => void; /** * Function to handle incoming messages from the WebSocket connection. @@ -406,8 +380,8 @@ class Client { 'x-sdk-name': 'Web', 'x-sdk-platform': 'client', 'x-sdk-language': 'web', - 'x-sdk-version': '24.3.0', - 'X-Appwrite-Response-Format': '1.9.1', + 'x-sdk-version': '25.0.0', + 'X-Appwrite-Response-Format': '1.9.2', }; /** @@ -584,10 +558,7 @@ class Client { url: '', channels: new Set(), subscriptions: new Map(), - slotToSubscriptionId: new Map(), - subscriptionIdToSlot: new Map(), - pendingSubscribeSlots: [], - subscriptionsCounter: 0, + pendingSubscribes: new Map(), reconnect: true, reconnectAttempts: 0, lastMessage: undefined, @@ -674,39 +645,21 @@ class Client { }, timeout); }) } else if (this.realtime.socket?.readyState === WebSocket.OPEN) { - // URL is unchanged; re-send subscribe message to apply updated queries. - this.realtime.sendSubscribeMessage(); + this.realtime.sendPendingSubscribes(); } }, - sendSubscribeMessage: () => { + sendPendingSubscribes: () => { if (!this.realtime.socket || this.realtime.socket.readyState !== WebSocket.OPEN) { return; } - const rows: RealtimeRequestSubscribe[] = []; - this.realtime.pendingSubscribeSlots = []; - - this.realtime.subscriptions.forEach((sub, slot) => { - const queries = sub.queries ?? []; - - const row: RealtimeRequestSubscribe = { - channels: sub.channels, - queries - }; - - const knownSubscriptionId = this.realtime.slotToSubscriptionId.get(slot); - if (knownSubscriptionId) { - row.subscriptionId = knownSubscriptionId; - } - - rows.push(row); - this.realtime.pendingSubscribeSlots.push(slot); - }); - - if (rows.length < 1) { + if (this.realtime.pendingSubscribes.size < 1) { return; } + const rows = Array.from(this.realtime.pendingSubscribes.values()); + this.realtime.pendingSubscribes.clear(); + this.realtime.socket.send(JSONbig.stringify({ type: 'subscribe', data: rows @@ -719,30 +672,7 @@ class Client { switch (message.type) { case 'connected': { const messageData = message.data; - if (messageData?.subscriptions) { - for (const [slotStr, subscriptionId] of Object.entries(messageData.subscriptions)) { - const slot = Number(slotStr); - if (isNaN(slot) || typeof subscriptionId !== 'string') { - continue; - } - const directSlotExists = this.realtime.subscriptions.has(slot); - const shiftedSlot = slot + 1; - const shiftedSlotExists = this.realtime.subscriptions.has(shiftedSlot); - const targetSlot = directSlotExists ? slot : shiftedSlotExists ? shiftedSlot : slot; - - this.realtime.slotToSubscriptionId.set(targetSlot, subscriptionId); - this.realtime.subscriptionIdToSlot.set(subscriptionId, targetSlot); - } - } - - this.realtime.subscriptions.forEach((_sub, slot) => { - const existing = this.realtime.slotToSubscriptionId.get(slot); - if (existing) { - this.realtime.subscriptionIdToSlot.set(existing, slot); - } - }); - let session = this.config.session; if (!session) { const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); @@ -757,27 +687,21 @@ class Client { })); } - this.realtime.sendSubscribeMessage(); - break; - } - case 'response': { - const action = message.data as RealtimeResponseAction; - if (action?.to !== 'subscribe' || !Array.isArray(action.subscriptions)) { - break; - } - - action.subscriptions.forEach((subscription, index) => { - const subscriptionId = subscription?.subscriptionId; - const slot = this.realtime.pendingSubscribeSlots[index]; - if (!subscriptionId || slot === undefined) { - return; - } - - this.realtime.slotToSubscriptionId.set(slot, subscriptionId); - this.realtime.subscriptionIdToSlot.set(subscriptionId, slot); + this.realtime.subscriptions.forEach((sub, subscriptionId) => { + this.realtime.pendingSubscribes.set(subscriptionId, { + subscriptionId, + channels: sub.channels, + queries: sub.queries ?? [] + }); }); + this.realtime.sendPendingSubscribes(); break; } + case 'response': + // The SDK generates subscriptionIds client-side and sends them on every + // subscribe/unsubscribe, so subscribe/unsubscribe acks carry no state + // the SDK needs to reconcile. + break; case 'event': { const data = >message.data; if (!data?.channels) break; @@ -785,12 +709,9 @@ class Client { const eventSubIds = data.subscriptions; if (eventSubIds && eventSubIds.length > 0) { for (const subscriptionId of eventSubIds) { - const slot = this.realtime.subscriptionIdToSlot.get(subscriptionId); - if (slot !== undefined) { - const subscription = this.realtime.subscriptions.get(slot); - if (subscription) { - setTimeout(() => subscription.callback(data)); - } + const subscription = this.realtime.subscriptions.get(subscriptionId); + if (subscription) { + setTimeout(() => subscription.callback(data)); } } } else { @@ -873,23 +794,35 @@ class Client { channelStrings.forEach(channel => this.realtime.channels.add(channel)); const queryStrings = (queries ?? []).map(q => typeof q === 'string' ? q : q.toString()); - const counter = this.realtime.subscriptionsCounter++; - this.realtime.subscriptions.set(counter, { + + let subscriptionId = ''; + const attempts = this.realtime.subscriptions.size + 1; + for (let i = 0; i < attempts; i++) { + const candidate = ID.unique(); + if (!this.realtime.subscriptions.has(candidate)) { + subscriptionId = candidate; + break; + } + } + if (subscriptionId === '') { + throw new AppwriteException('Failed to generate unique subscription id'); + } + this.realtime.subscriptions.set(subscriptionId, { channels: channelStrings, queries: queryStrings, callback }); + this.realtime.pendingSubscribes.set(subscriptionId, { + subscriptionId, + channels: channelStrings, + queries: queryStrings + }); this.realtime.connect(); return () => { - const subscriptionId = this.realtime.slotToSubscriptionId.get(counter); - this.realtime.subscriptions.delete(counter); - this.realtime.slotToSubscriptionId.delete(counter); - if (subscriptionId) { - this.realtime.subscriptionIdToSlot.delete(subscriptionId); - } - // Remove channels that are no longer referenced by any active subscription. + this.realtime.subscriptions.delete(subscriptionId); + this.realtime.pendingSubscribes.delete(subscriptionId); const stillUsed = new Set(); this.realtime.subscriptions.forEach(sub => { sub.channels.forEach(channel => stillUsed.add(channel)); diff --git a/src/models.ts b/src/models.ts index ef424e14..a22d5b76 100644 --- a/src/models.ts +++ b/src/models.ts @@ -987,6 +987,10 @@ export namespace Models { * User email address. Hide this attribute by toggling membership privacy in the Console. */ userEmail: string; + /** + * User phone number. Hide this attribute by toggling membership privacy in the Console. + */ + userPhone: string; /** * Team ID. */ diff --git a/src/services/realtime.ts b/src/services/realtime.ts index fb70dc16..a1368b9e 100644 --- a/src/services/realtime.ts +++ b/src/services/realtime.ts @@ -1,8 +1,29 @@ -import { AppwriteException, Client } from '../client'; +import { AppwriteException, Client, JSONbig } from '../client'; import { Channel, ActionableChannel, ResolvedChannel } from '../channel'; import { Query } from '../query'; +import { ID } from '../id'; + +export type RealtimeSubscriptionUpdate = { + channels?: (string | Channel | ActionableChannel | ResolvedChannel)[]; + queries?: (string | Query)[]; +} export type RealtimeSubscription = { + /** + * Remove this subscription only. Keeps the WebSocket open so other subscriptions keep receiving events. + * Use `Realtime.disconnect()` to close the connection entirely. + */ + unsubscribe: () => Promise; + + /** + * Replace the channels and/or queries for this subscription on the server without re-creating it. + */ + update: (changes: RealtimeSubscriptionUpdate) => Promise; + + /** + * Alias of `unsubscribe()` plus legacy auto-disconnect when this was the last active subscription. + * Prefer `unsubscribe()` for per-subscription teardown and `Realtime.disconnect()` for full shutdown. + */ close: () => Promise; } @@ -28,11 +49,10 @@ export type RealtimeResponseEvent = { export type RealtimeResponseConnected = { channels: string[]; user?: object; - subscriptions?: { [slot: string]: string }; // Map slot index -> subscriptionId } export type RealtimeRequest = { - type: 'authentication' | 'subscribe'; + type: 'authentication' | 'subscribe' | 'unsubscribe'; data: any; } @@ -42,16 +62,6 @@ type RealtimeRequestSubscribeRow = { queries: string[]; } -type RealtimeResponseAction = { - to?: string; - success?: boolean; - subscriptions?: Array<{ - subscriptionId?: string; - channels?: string[]; - queries?: string[]; - }>; -} - export enum RealtimeCode { NORMAL_CLOSURE = 1000, POLICY_VIOLATION = 1008, @@ -69,18 +79,12 @@ export class Realtime { private client: Client; private socket?: WebSocket; - // Slot-centric state: Map, queries: string[], callback: Function }> - private activeSubscriptions = new Map>(); - // Map slot index -> subscriptionId (from backend) - private slotToSubscriptionId = new Map(); - // Inverse map: subscriptionId -> slot index (for O(1) lookup) - private subscriptionIdToSlot = new Map(); + private activeSubscriptions = new Map>(); + private pendingSubscribes = new Map(); private heartbeatTimer?: number; private subCallDepth = 0; - private pendingSubscribeSlots: number[] = []; private reconnectAttempts = 0; - private subscriptionsCounter = 0; private connectionId = 0; private reconnect = true; @@ -124,16 +128,16 @@ export class Realtime { private startHeartbeat(): void { this.stopHeartbeat(); - this.heartbeatTimer = window.setInterval(() => { + this.heartbeatTimer = window?.setInterval(() => { if (this.socket && this.socket.readyState === WebSocket.OPEN) { - this.socket.send(JSON.stringify({ type: 'ping' })); + this.socket.send(JSONbig.stringify({ type: 'ping' })); } }, this.HEARTBEAT_INTERVAL); } private stopHeartbeat(): void { if (this.heartbeatTimer) { - window.clearInterval(this.heartbeatTimer); + window?.clearInterval(this.heartbeatTimer); this.heartbeatTimer = undefined; } } @@ -191,7 +195,7 @@ export class Realtime { return; } try { - const message = JSON.parse(event.data) as RealtimeResponse; + const message = JSONbig.parse(event.data) as RealtimeResponse; this.handleMessage(message); } catch (error) { console.error('Failed to parse message:', error); @@ -278,39 +282,67 @@ export class Realtime { return new Promise(resolve => setTimeout(resolve, ms)); } - private getKnownSubscriptionId(slot: number): string | undefined { - return this.slotToSubscriptionId.get(slot); - } - - private sendSubscribeMessage(): void { + private sendUnsubscribeMessage(subscriptionIds: string[]): void { + const ids = subscriptionIds.filter(id => typeof id === 'string' && id.length > 0); + if (ids.length === 0) { + return; + } if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { return; } + this.socket.send(JSONbig.stringify({ + type: 'unsubscribe', + data: ids.map(subscriptionId => ({ subscriptionId })) + })); + } - const rows: RealtimeRequestSubscribeRow[] = []; - this.pendingSubscribeSlots = []; + private generateUniqueSubscriptionId(): string { + const attempts = this.activeSubscriptions.size + 1; + for (let i = 0; i < attempts; i++) { + const id = ID.unique(); + if (!this.activeSubscriptions.has(id)) { + return id; + } + } + throw new AppwriteException('Failed to generate unique subscription id'); + } - for (const [slot, subscription] of this.activeSubscriptions) { - const queries = subscription.queries ?? []; + private enqueuePendingSubscribe(subscriptionId: string): void { + const subscription = this.activeSubscriptions.get(subscriptionId); + if (!subscription) { + return; + } + this.pendingSubscribes.set(subscriptionId, { + subscriptionId, + channels: Array.from(subscription.channels), + queries: subscription.queries ?? [] + }); + } - const row: RealtimeRequestSubscribeRow = { - channels: Array.from(subscription.channels), - queries - }; - const knownSubscriptionId = this.getKnownSubscriptionId(slot); - if (knownSubscriptionId) { - row.subscriptionId = knownSubscriptionId; - } + /** + * Close the WebSocket connection and drop all active subscriptions client-side. + * Use this instead of calling `unsubscribe()` on every subscription when you want to tear everything down. + */ + public async disconnect(): Promise { + this.activeSubscriptions.clear(); + this.pendingSubscribes.clear(); + this.reconnect = false; + await this.closeSocket(); + } - rows.push(row); - this.pendingSubscribeSlots.push(slot); + private sendPendingSubscribes(): void { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + return; } - if (rows.length < 1) { + if (this.pendingSubscribes.size < 1) { return; } - this.socket.send(JSON.stringify({ + const rows = Array.from(this.pendingSubscribes.values()); + this.pendingSubscribes.clear(); + + this.socket.send(JSONbig.stringify({ type: 'subscribe', data: rows })); @@ -413,58 +445,92 @@ export class Realtime { } } - // Allocate a new slot index - this.subscriptionsCounter++; - const slot = this.subscriptionsCounter; + const subscriptionId = this.generateUniqueSubscriptionId(); - // Store slot-centric data: channels, queries, and callback belong to the slot - // queries is stored as string[] (array of query strings) - // No channel mutation occurs here - channels are derived from slots in createSocket() - this.activeSubscriptions.set(slot, { + this.activeSubscriptions.set(subscriptionId, { channels, queries: queryStrings, callback }); + this.enqueuePendingSubscribe(subscriptionId); this.subCallDepth++; + try { + await this.sleep(this.DEBOUNCE_MS); - await this.sleep(this.DEBOUNCE_MS); - - if (this.subCallDepth === 1) { - if (!this.socket || this.socket.readyState > WebSocket.OPEN) { - await this.createSocket(); - } else if (this.socket.readyState === WebSocket.OPEN) { - this.sendSubscribeMessage(); + if (this.subCallDepth === 1) { + if (!this.socket || this.socket.readyState > WebSocket.OPEN) { + await this.createSocket(); + } else if (this.socket.readyState === WebSocket.OPEN) { + this.sendPendingSubscribes(); + } } + } finally { + this.subCallDepth--; } - this.subCallDepth--; + const unsubscribe = async (): Promise => { + if (!this.activeSubscriptions.has(subscriptionId)) { + return; + } + this.activeSubscriptions.delete(subscriptionId); + this.pendingSubscribes.delete(subscriptionId); + this.sendUnsubscribeMessage([subscriptionId]); + }; - return { - close: async () => { - const subscriptionId = this.slotToSubscriptionId.get(slot); - this.activeSubscriptions.delete(slot); - this.slotToSubscriptionId.delete(slot); - if (subscriptionId) { - this.subscriptionIdToSlot.delete(subscriptionId); - } - if (this.activeSubscriptions.size === 0) { - this.reconnect = false; - await this.closeSocket(); - return; + const update = async (changes: RealtimeSubscriptionUpdate): Promise => { + const subscription = this.activeSubscriptions.get(subscriptionId); + if (!subscription) { + return; + } + + if (changes.channels !== undefined) { + const nextChannelStrings = changes.channels.map(ch => this.channelToString(ch)); + subscription.channels = new Set(nextChannelStrings); + } + + if (changes.queries !== undefined) { + const nextQueries: string[] = []; + for (const q of changes.queries) { + if (Array.isArray(q)) { + for (const inner of q) { + nextQueries.push(typeof inner === 'string' ? inner : (inner as Query).toString()); + } + } else { + nextQueries.push(typeof q === 'string' ? q : q.toString()); + } } + subscription.queries = nextQueries; + } - if (!this.socket || this.socket.readyState > WebSocket.OPEN) { - await this.createSocket(); - } else if (this.socket.readyState === WebSocket.OPEN) { - this.sendSubscribeMessage(); + this.enqueuePendingSubscribe(subscriptionId); + + this.subCallDepth++; + try { + await this.sleep(this.DEBOUNCE_MS); + + if (this.subCallDepth === 1) { + if (!this.socket || this.socket.readyState > WebSocket.OPEN) { + await this.createSocket(); + } else if (this.socket.readyState === WebSocket.OPEN) { + this.sendPendingSubscribes(); + } } + } finally { + this.subCallDepth--; } }; - } - // cleanUp is no longer needed - slots are removed directly in subscribe().close() - // Channels are automatically rebuilt from remaining slots in createSocket() + const close = async (): Promise => { + await unsubscribe(); + if (this.activeSubscriptions.size === 0) { + this.reconnect = false; + await this.closeSocket(); + } + }; + + return { unsubscribe, update, close }; + } private handleMessage(message: RealtimeResponse): void { if (!message.type) { @@ -497,31 +563,10 @@ export class Realtime { const messageData = message.data as RealtimeResponseConnected; - // Store subscription ID mappings from backend. - // Use direct slot first; if URL mapping is zero-based, try slot+1. - if (messageData.subscriptions) { - for (const [slotStr, subscriptionId] of Object.entries(messageData.subscriptions)) { - const slot = Number(slotStr); - if (isNaN(slot)) { - continue; - } - - const directSlotExists = this.activeSubscriptions.has(slot); - const shiftedSlot = slot + 1; - const shiftedSlotExists = this.activeSubscriptions.has(shiftedSlot); - const targetSlot = directSlotExists ? slot : shiftedSlotExists ? shiftedSlot : slot; - - if (typeof subscriptionId === 'string') { - this.slotToSubscriptionId.set(targetSlot, subscriptionId); - this.subscriptionIdToSlot.set(subscriptionId, targetSlot); - } - } - } - let session = this.client.config.session; if (!session) { try { - const cookie = JSON.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); + const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); session = cookie?.[`a_session_${this.client.config.project}`]; } catch (error) { console.error('Failed to parse cookie fallback:', error); @@ -529,7 +574,7 @@ export class Realtime { } if (session && !messageData.user) { - this.socket?.send(JSON.stringify({ + this.socket?.send(JSONbig.stringify({ type: 'authentication', data: { session @@ -537,7 +582,10 @@ export class Realtime { })); } - this.sendSubscribeMessage(); + for (const subscriptionId of this.activeSubscriptions.keys()) { + this.enqueuePendingSubscribe(subscriptionId); + } + this.sendPendingSubscribes(); } private handleResponseError(message: RealtimeResponse): void { @@ -564,42 +612,24 @@ export class Realtime { return; } - // Iterate over all matching subscriptionIds and call callback for each for (const subscriptionId of subscriptions) { - // O(1) lookup using subscriptionId - const slot = this.subscriptionIdToSlot.get(subscriptionId); - if (slot !== undefined) { - const subscription = this.activeSubscriptions.get(slot); - if (subscription) { - const response: RealtimeResponseEvent = { - events, - channels, - timestamp, - payload, - subscriptions - }; - subscription.callback(response); - } + const subscription = this.activeSubscriptions.get(subscriptionId); + if (!subscription) { + continue; } + subscription.callback({ + events, + channels, + timestamp, + payload, + subscriptions + }); } } - private handleResponseAction(message: RealtimeResponse): void { - const data = message.data as RealtimeResponseAction | undefined; - if (!data || data.to !== 'subscribe' || !Array.isArray(data.subscriptions)) { - return; - } - - for (let i = 0; i < data.subscriptions.length; i++) { - const subscription = data.subscriptions[i]; - const subscriptionId = subscription?.subscriptionId; - const slot = this.pendingSubscribeSlots[i]; - if (slot === undefined || !subscriptionId) { - continue; - } - - this.slotToSubscriptionId.set(slot, subscriptionId); - this.subscriptionIdToSlot.set(subscriptionId, slot); - } + private handleResponseAction(_message: RealtimeResponse): void { + // The SDK generates subscriptionIds client-side and sends them on every + // subscribe/unsubscribe, so subscribe/unsubscribe acks carry no state + // the SDK needs to reconcile. } }