From a006409625488850df06ee9bc4f5927a55df7db2 Mon Sep 17 00:00:00 2001 From: Jose David Baena Date: Mon, 3 Aug 2026 11:19:00 +0200 Subject: [PATCH 1/6] fix(browser-sdk): restore complete validation baseline --- .eslintrc.json | 35 + .github/workflows/ci.yml | 5 +- package-lock.json | 1665 +++++++++++++++++++++++++++++++++++++- package.json | 9 +- src/client.ts | 78 +- src/storage.ts | 11 +- 6 files changed, 1770 insertions(+), 33 deletions(-) create mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..4f00a84 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,35 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module", + "project": "./tsconfig.json" + }, + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], + "env": { + "browser": true, + "es2022": true + }, + "rules": { + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/prefer-nullish-coalescing": "warn", + "@typescript-eslint/prefer-optional-chain": "warn", + "@typescript-eslint/strict-boolean-expressions": "off", + "no-console": "warn", + "no-debugger": "error", + "prefer-const": "error", + "no-var": "error", + "eqeqeq": ["error", "always"] + }, + "ignorePatterns": ["dist/", "node_modules/", "**/*.test.ts"] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d6a5bd..f38d7b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,10 @@ jobs: cache: npm - name: Install dependencies - run: npm install --no-audit --no-fund + run: npm ci --no-audit --no-fund + + - name: Lint + run: npm run lint - name: Type check run: npm run typecheck diff --git a/package-lock.json b/package-lock.json index 6be7230..cbd453f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,17 @@ { "name": "@streamlinelabs/browser-sdk", - "version": "0.1.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@streamlinelabs/browser-sdk", - "version": "0.1.0", + "version": "0.3.0", "license": "Apache-2.0", "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.57.1", "fake-indexeddb": "^5.0.2", "typescript": "^5.4.0", "vitest": "^1.6.0" @@ -408,6 +411,155 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "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": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "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": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -428,6 +580,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "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", @@ -792,6 +982,225 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, "node_modules/@vitest/expect": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", @@ -879,6 +1288,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.5", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", @@ -892,6 +1311,33 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -905,6 +1351,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -915,6 +1378,36 @@ "node": "*" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -925,6 +1418,16 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -944,18 +1447,78 @@ "node": ">=4" } }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "get-func-name": "^2.0.2" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": "*" - } + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" }, "node_modules/confbox": { "version": "0.1.8", @@ -1010,6 +1573,13 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -1020,6 +1590,32 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1059,6 +1655,184 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "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": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1069,6 +1843,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -1103,6 +1887,139 @@ "node": ">=18" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "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.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1141,6 +2058,119 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "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": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -1151,6 +2181,105 @@ "node": ">=16.17.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -1178,6 +2307,74 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/local-pkg": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", @@ -1195,6 +2392,29 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -1222,6 +2442,30 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -1235,6 +2479,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -1281,6 +2541,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -1310,6 +2577,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -1326,6 +2603,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", @@ -1342,6 +2637,84 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1352,6 +2725,16 @@ "node": ">=8" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -1376,6 +2759,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -1424,6 +2820,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -1439,6 +2845,37 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -1446,6 +2883,44 @@ "dev": true, "license": "MIT" }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -1491,6 +2966,43 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1534,6 +3046,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1558,6 +3080,19 @@ "dev": true, "license": "MIT" }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -1571,6 +3106,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-literal": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", @@ -1584,6 +3132,26 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -1611,6 +3179,45 @@ "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", @@ -1621,6 +3228,19 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1642,6 +3262,16 @@ "dev": true, "license": "MIT" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -1824,6 +3454,23 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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" + }, "node_modules/yocto-queue": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", diff --git a/package.json b/package.json index 8228969..4ab90ec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@streamlinelabs/browser-sdk", "version": "0.3.0", - "description": "Streamline browser SDK \u2014 produce/consume directly from the browser via WebSocket/WebTransport, with IndexedDB persistence and CRDT sync (M3, Experimental).", + "description": "Streamline browser SDK — produce/consume directly from the browser via WebSocket/WebTransport, with IndexedDB persistence and CRDT sync (M3, Experimental).", "type": "module", "main": "dist/index.js", "module": "dist/index.js", @@ -34,8 +34,11 @@ "node": ">=18" }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.57.1", + "fake-indexeddb": "^5.0.2", "typescript": "^5.4.0", - "vitest": "^1.6.0", - "fake-indexeddb": "^5.0.2" + "vitest": "^1.6.0" } } diff --git a/src/client.ts b/src/client.ts index 47ab79a..471078d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -103,7 +103,11 @@ export class Client { this.socket = sock; this.transportKind = "websocket"; - sock.onmessage = (ev: MessageEvent) => this.handleIncoming(ev.data); + sock.onmessage = (event: MessageEvent) => { + if (typeof event.data === "string" || event.data instanceof ArrayBuffer) { + this.handleIncoming(event.data); + } + }; sock.onclose = () => this.handleDisconnect(); resolve(); @@ -144,10 +148,10 @@ export class Client { this.transportKind = "webtransport"; // Start background read loop for incoming records. - this.readWebTransportLoop(); + void this.readWebTransportLoop(); // Auto-reconnect when the session closes. - conn.closed.then(() => this.handleDisconnect()).catch(() => this.handleDisconnect()); + void conn.closed.then(() => this.handleDisconnect()).catch(() => this.handleDisconnect()); } /** Continuously read from the WebTransport bidi stream and dispatch. */ @@ -172,7 +176,7 @@ export class Client { this.connected = false; if (this.reconnecting) return; this.reconnecting = true; - this.autoReconnect(); + void this.autoReconnect(); } private async autoReconnect(): Promise { @@ -222,22 +226,18 @@ export class Client { private handleIncoming(data: ArrayBuffer | string): void { try { const text = typeof data === "string" ? data : new TextDecoder().decode(data); - const parsed = JSON.parse(text); + const parsed: unknown = JSON.parse(text); + if (!isObjectRecord(parsed)) return; // Normalise into a Record. const rec: Record = { - topic: parsed.topic ?? "", - partition: parsed.partition ?? 0, - offset: BigInt(parsed.offset ?? 0), - key: parsed.key ? new Uint8Array(parsed.key) : undefined, - value: - parsed.value instanceof Uint8Array - ? parsed.value - : new TextEncoder().encode( - typeof parsed.value === "string" ? parsed.value : JSON.stringify(parsed.value), - ), - timestampMs: parsed.timestampMs ?? Date.now(), - headers: parsed.headers, + topic: typeof parsed.topic === "string" ? parsed.topic : "", + partition: typeof parsed.partition === "number" ? parsed.partition : 0, + offset: parseOffset(parsed.offset), + key: parseBytes(parsed.key), + value: parseValue(parsed.value), + timestampMs: typeof parsed.timestampMs === "number" ? parsed.timestampMs : Date.now(), + headers: parseHeaders(parsed.headers), }; const listeners = this.topicListeners.get(rec.topic); @@ -357,7 +357,7 @@ export class Client { this.reconnecting = true; // prevent reconnect after close this.socket?.close(); try { - this.wtWriter?.close(); + await this.wtWriter?.close(); } catch { /* already closed */ } try { this.wtConn?.close(); @@ -374,3 +374,45 @@ export class Client { return new Promise((r) => setTimeout(r, ms)); } } + +function isObjectRecord(value: unknown): value is { [key: string]: unknown } { + return typeof value === "object" && value !== null; +} + +function parseOffset(value: unknown): bigint { + if (typeof value === "bigint" || typeof value === "number" || typeof value === "string") { + try { + return BigInt(value); + } catch { + return 0n; + } + } + return 0n; +} + +function parseBytes(value: unknown): Uint8Array | undefined { + if (value instanceof Uint8Array) return value; + if (value instanceof ArrayBuffer) return new Uint8Array(value); + if (Array.isArray(value) && value.every((item) => typeof item === "number")) { + return new Uint8Array(value); + } + return undefined; +} + +function parseValue(value: unknown): Uint8Array { + const bytes = parseBytes(value); + if (bytes) return bytes; + + const text = typeof value === "string" ? value : JSON.stringify(value) ?? ""; + return new TextEncoder().encode(text); +} + +function parseHeaders(value: unknown): { [key: string]: string } | undefined { + if (!isObjectRecord(value)) return undefined; + + const headers = Object.entries(value); + if (!headers.every((entry): entry is [string, string] => typeof entry[1] === "string")) { + return undefined; + } + return Object.fromEntries(headers); +} diff --git a/src/storage.ts b/src/storage.ts index 3948f9d..6ce14c4 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -114,6 +114,13 @@ function serialize(rec: Record): unknown { return { ...rec, offset: rec.offset.toString() }; } -function deserialize(raw: any): Record { - return { ...raw, offset: BigInt(raw.offset) }; +function deserialize(raw: unknown): Record { + if (typeof raw !== "object" || raw === null || !("offset" in raw)) { + throw new TypeError("Stored record is missing an offset"); + } + + const stored = raw as Omit & { + offset: string | number | bigint; + }; + return { ...stored, offset: BigInt(stored.offset) }; } From b9463ba76081d955cd256bf1a13655dbe2e9ff36 Mon Sep 17 00:00:00 2001 From: Jose David Baena Date: Mon, 3 Aug 2026 11:20:16 +0200 Subject: [PATCH 2/6] docs(browser-sdk): record clean-code audit --- AUDIT.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 AUDIT.md diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..9caf34f --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,67 @@ +# Clean Code and SRP Audit + +## Summary + +- **Highest-leverage split:** remove WebSocket/WebTransport ownership from + `Client` into two transport implementations behind one internal transport + contract. +- `Client` currently changes for browser transport APIs, reconnect policy, + incoming wire decoding, topic subscriptions, offline persistence, and drain + orchestration. +- Incoming frame normalization is a meaningful protocol decision and should be + independently tested outside transport callbacks. +- `LocalStore` is cohesive despite managing two object stores: both exist for + one IndexedDB persistence actor and share one database lifecycle. +- CRDT and Moonshot modules are long enough to inspect but each has one actor + and should remain independent. + +## Findings + +| ID | Location | Category | Severity | Actors in conflict | Cost | Size | Behavior risk | +|---|---|---|---|---|---|---|---| +| BROWSER-SRP-1 | `src/client.ts:36-375` | SRP, mixed class | P1 | browser transport maintainers; reconnect policy; wire protocol; offline product; topic consumers | A browser API change and a persistence/drain change edit the same stateful client and lifecycle methods. | L | Medium | +| BROWSER-SRP-2 | `src/client.ts:226-245,378-417` | Function/module SRP | P2 | wire-contract consumers; transport callbacks | JSON parsing, validation/defaults, byte conversion, and listener dispatch are interleaved; malformed frames are silently discarded at the transport boundary. | M | Medium | +| BROWSER-CC-1 | `src/client.ts:175-196` | Hidden lifecycle side effect | P2 | application lifecycle; reconnect policy | Any close starts an unbounded reconnect loop unless the separate `reconnecting` flag was set by `close()`. | S | Medium | + +## Actor and State Partition + +| Partition | Fields/methods | Actor | +|---|---|---| +| Transport state | socket, WebTransport connection/reader/writer, connect/send/read/close | browser platform | +| Reconnect state | connected, reconnecting, delay policy | reliability | +| Wire codec | incoming JSON normalization, byte/header/offset conversion | protocol | +| Subscriptions | topic listener map, subscribe/unsubscribe/dispatch | SDK consumer | +| Offline drain | LocalStore, draining guard, pending replay/progress | offline-first product | + +Resulting internal units: + +- `WebSocketTransport` and `WebTransportTransport`, both owning real connection + state and implementing the same internal transport operations. +- `decodeIncomingRecord` in `wire.ts`, returning a typed record or `undefined`. +- `Client` retaining reconnect, subscription, offline drain, and public API + orchestration. + +## Ordered Refactor Sequence + +1. Characterize incoming record decoding and both transport lifecycle paths. +2. Move wire normalization unchanged into `wire.ts`. +3. Improve the decoder using explicit typed boundaries while preserving current + defaults and malformed-frame behavior. +4. Move WebSocket and WebTransport state/methods into two transport units. +5. Keep reconnect and offline draining in `Client`; do not create a forwarding + service layer. +6. Run tests, lint, typecheck, and build after every commit. + +## Deferred + +- Reconnect attempt limits and cancellation policy are product behavior and are + not changed by this structural pass. +- WebTransport integration requires a real browser/server environment; unit + tests use platform fakes. + +## Out of Scope + +- `LocalStore`: one persistence actor and one database lifecycle. +- `Topic`: one topic-handle abstraction over local history and live tailing. +- `LWWRegister`/HLC code: one CRDT actor. +- Moonshot read-only clients: one gateway-safe HTTP contract. From b311c3e224ae7e785ca24216b2646688d1394342 Mon Sep 17 00:00:00 2001 From: Jose David Baena Date: Mon, 3 Aug 2026 11:22:45 +0200 Subject: [PATCH 3/6] test(browser-sdk): characterize incoming records --- src/client.test.ts | 101 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/client.test.ts b/src/client.test.ts index ef8e3d4..05defb0 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -130,4 +130,105 @@ describe("Client", () => { const c = new Client({ url: "ws://localhost:9092", clientId: "c" }); expect(c.isConnected).toBe(false); }); + + it("normalizes incoming WebSocket records before dispatch", async () => { + const originalWebSocket = globalThis.WebSocket; + let socket: FakeWebSocket | undefined; + class FakeWebSocket { + binaryType = ""; + onopen: (() => void) | null = null; + onmessage: ((event: { data: unknown }) => void) | null = null; + onclose: (() => void) | null = null; + onerror: ((error: unknown) => void) | null = null; + readonly sent: unknown[] = []; + + constructor(_url: string) { + socket = this; + } + + send(value: unknown): void { + this.sent.push(value); + } + + close(): void {} + } + + (globalThis as Record).WebSocket = FakeWebSocket; + try { + const client = new Client({ + url: "ws://localhost:9092", + clientId: "incoming", + preferTransport: "websocket", + }); + const connecting = client.connect(); + socket?.onopen?.(); + await connecting; + + const received: Array<{ + topic: string; + partition: number; + offset: bigint; + value: Uint8Array; + }> = []; + client.subscribe("events", (record) => received.push(record)); + socket?.onmessage?.({ + data: JSON.stringify({ + topic: "events", + partition: 2, + offset: "42", + value: [104, 105], + timestampMs: 123, + }), + }); + + expect(received).toHaveLength(1); + expect(received[0]?.topic).toBe("events"); + expect(received[0]?.partition).toBe(2); + expect(received[0]?.offset).toBe(42n); + expect(Array.from(received[0]?.value ?? [])).toEqual([104, 105]); + await client.close(); + } finally { + globalThis.WebSocket = originalWebSocket; + } + }); + + it("ignores malformed incoming WebSocket frames", async () => { + const originalWebSocket = globalThis.WebSocket; + let socket: FakeWebSocket | undefined; + class FakeWebSocket { + binaryType = ""; + onopen: (() => void) | null = null; + onmessage: ((event: { data: unknown }) => void) | null = null; + onclose: (() => void) | null = null; + onerror: ((error: unknown) => void) | null = null; + + constructor(_url: string) { + socket = this; + } + + send(_value: unknown): void {} + close(): void {} + } + + (globalThis as Record).WebSocket = FakeWebSocket; + try { + const client = new Client({ + url: "ws://localhost:9092", + clientId: "malformed", + preferTransport: "websocket", + }); + const connecting = client.connect(); + socket?.onopen?.(); + await connecting; + + const listener = vi.fn(); + client.subscribe("events", listener); + socket?.onmessage?.({ data: "{not json" }); + + expect(listener).not.toHaveBeenCalled(); + await client.close(); + } finally { + globalThis.WebSocket = originalWebSocket; + } + }); }); From 17f179ef567c5c9fd846a5e52ad38b82b7322573 Mon Sep 17 00:00:00 2001 From: Jose David Baena Date: Mon, 3 Aug 2026 11:25:18 +0200 Subject: [PATCH 4/6] refactor(browser-sdk): move wire normalization (BROWSER-SRP-2) --- src/client.ts | 49 +++++++------------------------------------------ src/wire.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 42 deletions(-) create mode 100644 src/wire.ts diff --git a/src/client.ts b/src/client.ts index 471078d..ca9b7a8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -14,6 +14,13 @@ import { LocalStore } from "./storage.js"; import { Topic } from "./topic.js"; import type { Record, DrainProgress } from "./types.js"; import { StreamlineError, StreamlineErrorCode, validateTopicName } from "./types.js"; +import { + isObjectRecord, + parseBytes, + parseHeaders, + parseOffset, + parseValue, +} from "./wire.js"; export interface ClientOptions { /** ws(s):// or https:// (WebTransport) URL of the broker. */ @@ -374,45 +381,3 @@ export class Client { return new Promise((r) => setTimeout(r, ms)); } } - -function isObjectRecord(value: unknown): value is { [key: string]: unknown } { - return typeof value === "object" && value !== null; -} - -function parseOffset(value: unknown): bigint { - if (typeof value === "bigint" || typeof value === "number" || typeof value === "string") { - try { - return BigInt(value); - } catch { - return 0n; - } - } - return 0n; -} - -function parseBytes(value: unknown): Uint8Array | undefined { - if (value instanceof Uint8Array) return value; - if (value instanceof ArrayBuffer) return new Uint8Array(value); - if (Array.isArray(value) && value.every((item) => typeof item === "number")) { - return new Uint8Array(value); - } - return undefined; -} - -function parseValue(value: unknown): Uint8Array { - const bytes = parseBytes(value); - if (bytes) return bytes; - - const text = typeof value === "string" ? value : JSON.stringify(value) ?? ""; - return new TextEncoder().encode(text); -} - -function parseHeaders(value: unknown): { [key: string]: string } | undefined { - if (!isObjectRecord(value)) return undefined; - - const headers = Object.entries(value); - if (!headers.every((entry): entry is [string, string] => typeof entry[1] === "string")) { - return undefined; - } - return Object.fromEntries(headers); -} diff --git a/src/wire.ts b/src/wire.ts new file mode 100644 index 0000000..7fbf735 --- /dev/null +++ b/src/wire.ts @@ -0,0 +1,41 @@ +export function isObjectRecord(value: unknown): value is { [key: string]: unknown } { + return typeof value === "object" && value !== null; +} + +export function parseOffset(value: unknown): bigint { + if (typeof value === "bigint" || typeof value === "number" || typeof value === "string") { + try { + return BigInt(value); + } catch { + return 0n; + } + } + return 0n; +} + +export function parseBytes(value: unknown): Uint8Array | undefined { + if (value instanceof Uint8Array) return value; + if (value instanceof ArrayBuffer) return new Uint8Array(value); + if (Array.isArray(value) && value.every((item) => typeof item === "number")) { + return new Uint8Array(value); + } + return undefined; +} + +export function parseValue(value: unknown): Uint8Array { + const bytes = parseBytes(value); + if (bytes) return bytes; + + const text = typeof value === "string" ? value : JSON.stringify(value) ?? ""; + return new TextEncoder().encode(text); +} + +export function parseHeaders(value: unknown): { [key: string]: string } | undefined { + if (!isObjectRecord(value)) return undefined; + + const headers = Object.entries(value); + if (!headers.every((entry): entry is [string, string] => typeof entry[1] === "string")) { + return undefined; + } + return Object.fromEntries(headers); +} From 1f367ca54bc6f00764f5819cd74118853eaa3459 Mon Sep 17 00:00:00 2001 From: Jose David Baena Date: Mon, 3 Aug 2026 11:29:11 +0200 Subject: [PATCH 5/6] refactor(browser-sdk): isolate wire decoding (BROWSER-SRP-2) --- src/client.ts | 36 +++++++-------------------------- src/wire.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ src/wire.ts | 22 ++++++++++++++++++++ 3 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 src/wire.test.ts diff --git a/src/client.ts b/src/client.ts index ca9b7a8..15339ee 100644 --- a/src/client.ts +++ b/src/client.ts @@ -14,13 +14,7 @@ import { LocalStore } from "./storage.js"; import { Topic } from "./topic.js"; import type { Record, DrainProgress } from "./types.js"; import { StreamlineError, StreamlineErrorCode, validateTopicName } from "./types.js"; -import { - isObjectRecord, - parseBytes, - parseHeaders, - parseOffset, - parseValue, -} from "./wire.js"; +import { decodeIncomingRecord } from "./wire.js"; export interface ClientOptions { /** ws(s):// or https:// (WebTransport) URL of the broker. */ @@ -231,30 +225,14 @@ export class Client { * Expects a JSON-encoded record envelope. */ private handleIncoming(data: ArrayBuffer | string): void { - try { - const text = typeof data === "string" ? data : new TextDecoder().decode(data); - const parsed: unknown = JSON.parse(text); - if (!isObjectRecord(parsed)) return; - - // Normalise into a Record. - const rec: Record = { - topic: typeof parsed.topic === "string" ? parsed.topic : "", - partition: typeof parsed.partition === "number" ? parsed.partition : 0, - offset: parseOffset(parsed.offset), - key: parseBytes(parsed.key), - value: parseValue(parsed.value), - timestampMs: typeof parsed.timestampMs === "number" ? parsed.timestampMs : Date.now(), - headers: parseHeaders(parsed.headers), - }; + const record = decodeIncomingRecord(data); + if (!record) return; - const listeners = this.topicListeners.get(rec.topic); - if (listeners) { - for (const cb of listeners) { - cb(rec); - } + const listeners = this.topicListeners.get(record.topic); + if (listeners) { + for (const callback of listeners) { + callback(record); } - } catch { - // Malformed frame — silently ignore. } } diff --git a/src/wire.test.ts b/src/wire.test.ts new file mode 100644 index 0000000..84f341c --- /dev/null +++ b/src/wire.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { decodeIncomingRecord } from "./wire.js"; + +describe("decodeIncomingRecord", () => { + it("decodes string envelopes", () => { + const record = decodeIncomingRecord(JSON.stringify({ + topic: "events", + partition: 2, + offset: "42", + key: [1, 2], + value: [104, 105], + timestampMs: 123, + headers: { source: "test" }, + })); + + expect(record).toEqual({ + topic: "events", + partition: 2, + offset: 42n, + key: new Uint8Array([1, 2]), + value: new Uint8Array([104, 105]), + timestampMs: 123, + headers: { source: "test" }, + }); + }); + + it("decodes ArrayBuffer envelopes", () => { + const data = new TextEncoder().encode(JSON.stringify({ + topic: "events", + value: "hello", + })); + + const record = decodeIncomingRecord(data.buffer); + expect(record?.topic).toBe("events"); + expect(new TextDecoder().decode(record?.value)).toBe("hello"); + }); + + it("preserves current defaults for missing fields", () => { + const before = Date.now(); + const record = decodeIncomingRecord("{}"); + + expect(record?.topic).toBe(""); + expect(record?.partition).toBe(0); + expect(record?.offset).toBe(0n); + expect(record?.value).toEqual(new Uint8Array()); + expect(record?.timestampMs).toBeGreaterThanOrEqual(before); + }); + + it("returns undefined for malformed frames", () => { + expect(decodeIncomingRecord("{not json")).toBeUndefined(); + }); +}); diff --git a/src/wire.ts b/src/wire.ts index 7fbf735..daf917d 100644 --- a/src/wire.ts +++ b/src/wire.ts @@ -1,3 +1,25 @@ +import type { Record } from "./types.js"; + +export function decodeIncomingRecord(data: ArrayBuffer | string): Record | undefined { + try { + const text = typeof data === "string" ? data : new TextDecoder().decode(data); + const parsed: unknown = JSON.parse(text); + if (!isObjectRecord(parsed)) return undefined; + + return { + topic: typeof parsed.topic === "string" ? parsed.topic : "", + partition: typeof parsed.partition === "number" ? parsed.partition : 0, + offset: parseOffset(parsed.offset), + key: parseBytes(parsed.key), + value: parseValue(parsed.value), + timestampMs: typeof parsed.timestampMs === "number" ? parsed.timestampMs : Date.now(), + headers: parseHeaders(parsed.headers), + }; + } catch { + return undefined; + } +} + export function isObjectRecord(value: unknown): value is { [key: string]: unknown } { return typeof value === "object" && value !== null; } From 6d4fce54932adb47ca83658e711850d6a00af7e5 Mon Sep 17 00:00:00 2001 From: Jose David Baena Date: Mon, 3 Aug 2026 11:42:47 +0200 Subject: [PATCH 6/6] refactor(browser-sdk): extract transport actors (BROWSER-SRP-1) --- src/client.ts | 136 +++------------ src/transport.test.ts | 383 ++++++++++++++++++++++++++++++++++++++++++ src/transport.ts | 159 ++++++++++++++++++ 3 files changed, 564 insertions(+), 114 deletions(-) create mode 100644 src/transport.test.ts create mode 100644 src/transport.ts diff --git a/src/client.ts b/src/client.ts index 15339ee..4eea6b0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -15,6 +15,8 @@ import { Topic } from "./topic.js"; import type { Record, DrainProgress } from "./types.js"; import { StreamlineError, StreamlineErrorCode, validateTopicName } from "./types.js"; import { decodeIncomingRecord } from "./wire.js"; +import type { Transport, TransportKind } from "./transport.js"; +import { WebSocketTransport, WebTransportTransport } from "./transport.js"; export interface ClientOptions { /** ws(s):// or https:// (WebTransport) URL of the broker. */ @@ -29,24 +31,14 @@ export interface ClientOptions { reconnectDelayMs?: number; } -type TransportKind = "webtransport" | "websocket"; - /** Callback invoked during pending write drain. */ export type DrainCallback = (progress: DrainProgress) => void; export class Client { - private socket?: WebSocket; + private transport?: Transport; private store: LocalStore; private opts: ClientOptions; - /** Active transport type after connect(). */ - private transportKind?: TransportKind; - - // WebTransport bookkeeping - private wtConn?: WebTransport; - private wtWriter?: WritableStreamDefaultWriter; - private wtReader?: ReadableStreamDefaultReader; - /** Whether the client currently has a live transport to the broker. */ private connected = false; @@ -70,12 +62,12 @@ export class Client { /** Connect; returns a promise that resolves when the broker handshake completes. */ async connect(): Promise { - const wt = this.opts.preferTransport ?? this.detectTransport(); - if (wt === "webtransport" && "WebTransport" in globalThis) { - await this.connectWebTransport(); - } else { - await this.connectWebSocket(); - } + const transport = this.createTransport(this.opts.preferTransport ?? this.detectTransport()); + await transport.connect({ + onFrame: (data) => this.handleIncoming(data), + onDisconnect: () => this.handleDisconnect(), + }); + this.transport = transport; this.connected = true; // Drain any writes buffered while offline. @@ -88,85 +80,12 @@ export class Client { return "WebTransport" in globalThis ? "webtransport" : "websocket"; } - // -------------------------------------------------------------------------- - // WebSocket transport - // -------------------------------------------------------------------------- - - private connectWebSocket(): Promise { - return new Promise((resolve, reject) => { - const url = this.opts.url.replace(/^http/, "ws"); - const sock = new WebSocket(url); - sock.binaryType = "arraybuffer"; - sock.onopen = () => { - if (this.opts.token) { - sock.send(JSON.stringify({ type: "auth", token: this.opts.token })); - } - this.socket = sock; - this.transportKind = "websocket"; - - sock.onmessage = (event: MessageEvent) => { - if (typeof event.data === "string" || event.data instanceof ArrayBuffer) { - this.handleIncoming(event.data); - } - }; - sock.onclose = () => this.handleDisconnect(); - - resolve(); - }; - sock.onerror = (e) => reject(new StreamlineError( - `WebSocket connection failed: ${String(e)}`, - StreamlineErrorCode.Transport, - { retryable: true, hint: "Check that the Streamline server is running and accessible" }, - )); - }); - } - - // -------------------------------------------------------------------------- - // WebTransport transport - // -------------------------------------------------------------------------- - - /** - * Open a WebTransport session to the broker, create a bidirectional stream - * for the Streamline wire protocol, and send the auth token if configured. - */ - private async connectWebTransport(): Promise { - const WT = (globalThis as unknown as { WebTransport: typeof WebTransport }).WebTransport; - const conn = new WT(this.opts.url); - await conn.ready; - this.wtConn = conn; - - const bidi = await conn.createBidirectionalStream(); - this.wtWriter = bidi.writable.getWriter(); - this.wtReader = bidi.readable.getReader(); - - if (this.opts.token) { - const authFrame = new TextEncoder().encode( - JSON.stringify({ type: "auth", token: this.opts.token }), - ); - await this.wtWriter.write(authFrame); - } - - this.transportKind = "webtransport"; - - // Start background read loop for incoming records. - void this.readWebTransportLoop(); - - // Auto-reconnect when the session closes. - void conn.closed.then(() => this.handleDisconnect()).catch(() => this.handleDisconnect()); - } - - /** Continuously read from the WebTransport bidi stream and dispatch. */ - private async readWebTransportLoop(): Promise { - if (!this.wtReader) return; - try { - for (;;) { - const { value, done } = await this.wtReader.read(); - if (done) break; - this.handleIncoming(value.buffer as ArrayBuffer); - } - } catch { - // Stream broken — handled via handleDisconnect. + /** Instantiate the transport implementation for the resolved transport kind. */ + private createTransport(kind: TransportKind): Transport { + if (kind === "webtransport" && "WebTransport" in globalThis) { + return new WebTransportTransport(this.opts.url, this.opts.token); } + return new WebSocketTransport(this.opts.url, this.opts.token); } // -------------------------------------------------------------------------- @@ -205,19 +124,14 @@ export class Client { * Throws if no transport is connected. */ async send(data: ArrayBuffer): Promise { - if (this.transportKind === "websocket" && this.socket) { - this.socket.send(data); - return; - } - if (this.transportKind === "webtransport" && this.wtWriter) { - await this.wtWriter.write(new Uint8Array(data)); - return; + if (!this.transport) { + throw new StreamlineError( + "No active transport — call connect() first", + StreamlineErrorCode.Connection, + { retryable: true }, + ); } - throw new StreamlineError( - "No active transport — call connect() first", - StreamlineErrorCode.Connection, - { retryable: true }, - ); + await this.transport.send(data); } /** @@ -340,13 +254,7 @@ export class Client { async close(): Promise { this.connected = false; this.reconnecting = true; // prevent reconnect after close - this.socket?.close(); - try { - await this.wtWriter?.close(); - } catch { /* already closed */ } - try { - this.wtConn?.close(); - } catch { /* already closed */ } + await this.transport?.close(); await this.store.close(); } diff --git a/src/transport.test.ts b/src/transport.test.ts new file mode 100644 index 0000000..bea352c --- /dev/null +++ b/src/transport.test.ts @@ -0,0 +1,383 @@ +import { describe, it, expect, vi } from "vitest"; +import { WebSocketTransport, WebTransportTransport } from "./transport.js"; +import { StreamlineError } from "./types.js"; + +// -------------------------------------------------------------------------- +// WebSocketTransport +// -------------------------------------------------------------------------- + +class FakeWebSocket { + static instances: FakeWebSocket[] = []; + + binaryType = ""; + onopen: (() => void) | null = null; + onmessage: ((event: { data: unknown }) => void) | null = null; + onclose: (() => void) | null = null; + onerror: ((error: unknown) => void) | null = null; + readonly sent: unknown[] = []; + closed = false; + + constructor(public readonly url: string) { + FakeWebSocket.instances.push(this); + } + + send(value: unknown): void { + this.sent.push(value); + } + + close(): void { + this.closed = true; + } +} + +describe("WebSocketTransport", () => { + it("converts http(s) URLs to ws(s) and sets binaryType", async () => { + (globalThis as Record).WebSocket = FakeWebSocket; + FakeWebSocket.instances = []; + const transport = new WebSocketTransport("https://broker.example:9092"); + const connecting = transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + const sock = FakeWebSocket.instances[0]!; + expect(sock.url).toBe("wss://broker.example:9092"); + expect(sock.binaryType).toBe("arraybuffer"); + sock.onopen?.(); + await connecting; + delete (globalThis as Record).WebSocket; + }); + + it("sends an auth frame on open when a token is configured", async () => { + (globalThis as Record).WebSocket = FakeWebSocket; + FakeWebSocket.instances = []; + const transport = new WebSocketTransport("ws://broker:9092", "secret-token"); + const connecting = transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + const sock = FakeWebSocket.instances[0]!; + sock.onopen?.(); + await connecting; + + expect(sock.sent).toEqual([JSON.stringify({ type: "auth", token: "secret-token" })]); + delete (globalThis as Record).WebSocket; + }); + + it("does not send an auth frame when no token is configured", async () => { + (globalThis as Record).WebSocket = FakeWebSocket; + FakeWebSocket.instances = []; + const transport = new WebSocketTransport("ws://broker:9092"); + const connecting = transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + const sock = FakeWebSocket.instances[0]!; + sock.onopen?.(); + await connecting; + + expect(sock.sent).toEqual([]); + delete (globalThis as Record).WebSocket; + }); + + it("forwards string and ArrayBuffer messages via onFrame, ignoring other types", async () => { + (globalThis as Record).WebSocket = FakeWebSocket; + FakeWebSocket.instances = []; + const onFrame = vi.fn(); + const transport = new WebSocketTransport("ws://broker:9092"); + const connecting = transport.connect({ onFrame, onDisconnect: vi.fn() }); + const sock = FakeWebSocket.instances[0]!; + sock.onopen?.(); + await connecting; + + const buf = new ArrayBuffer(4); + sock.onmessage?.({ data: "hello" }); + sock.onmessage?.({ data: buf }); + sock.onmessage?.({ data: 42 }); // unsupported type, filtered out + sock.onmessage?.({ data: { not: "supported" } }); + + expect(onFrame).toHaveBeenCalledTimes(2); + expect(onFrame).toHaveBeenNthCalledWith(1, "hello"); + expect(onFrame).toHaveBeenNthCalledWith(2, buf); + delete (globalThis as Record).WebSocket; + }); + + it("invokes onDisconnect when the socket closes", async () => { + (globalThis as Record).WebSocket = FakeWebSocket; + FakeWebSocket.instances = []; + const onDisconnect = vi.fn(); + const transport = new WebSocketTransport("ws://broker:9092"); + const connecting = transport.connect({ onFrame: vi.fn(), onDisconnect }); + const sock = FakeWebSocket.instances[0]!; + sock.onopen?.(); + await connecting; + + sock.onclose?.(); + expect(onDisconnect).toHaveBeenCalledTimes(1); + delete (globalThis as Record).WebSocket; + }); + + it("rejects connect() with a StreamlineError on socket error", async () => { + (globalThis as Record).WebSocket = FakeWebSocket; + FakeWebSocket.instances = []; + const transport = new WebSocketTransport("ws://broker:9092"); + const connecting = transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + const sock = FakeWebSocket.instances[0]!; + sock.onerror?.(new Error("boom")); + + await expect(connecting).rejects.toThrow(StreamlineError); + await expect(connecting).rejects.toThrow(/WebSocket connection failed/); + delete (globalThis as Record).WebSocket; + }); + + it("sends binary frames over the open socket", async () => { + (globalThis as Record).WebSocket = FakeWebSocket; + FakeWebSocket.instances = []; + const transport = new WebSocketTransport("ws://broker:9092"); + const connecting = transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + const sock = FakeWebSocket.instances[0]!; + sock.onopen?.(); + await connecting; + + const payload = new ArrayBuffer(8); + await transport.send(payload); + expect(sock.sent).toContain(payload); + delete (globalThis as Record).WebSocket; + }); + + it("close() closes the underlying socket", async () => { + (globalThis as Record).WebSocket = FakeWebSocket; + FakeWebSocket.instances = []; + const transport = new WebSocketTransport("ws://broker:9092"); + const connecting = transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + const sock = FakeWebSocket.instances[0]!; + sock.onopen?.(); + await connecting; + + await transport.close(); + expect(sock.closed).toBe(true); + delete (globalThis as Record).WebSocket; + }); + + it("send() before connect() is a no-op (no throw)", async () => { + const transport = new WebSocketTransport("ws://broker:9092"); + await expect(transport.send(new ArrayBuffer(1))).resolves.toBeUndefined(); + }); + + it("close() before connect() is a no-op (no throw)", async () => { + const transport = new WebSocketTransport("ws://broker:9092"); + await expect(transport.close()).resolves.toBeUndefined(); + }); + + it("exposes kind = 'websocket'", () => { + const transport = new WebSocketTransport("ws://broker:9092"); + expect(transport.kind).toBe("websocket"); + }); +}); + +// -------------------------------------------------------------------------- +// WebTransportTransport +// -------------------------------------------------------------------------- + +/** Minimal fake WHATWG WritableStreamDefaultWriter. */ +function makeFakeWriter() { + const writes: Uint8Array[] = []; + let closed = false; + return { + writes, + get closed() { + return closed; + }, + write: vi.fn(async (chunk: Uint8Array) => { + writes.push(chunk); + }), + close: vi.fn(async () => { + closed = true; + }), + }; +} + +/** Minimal fake WHATWG ReadableStreamDefaultReader that yields queued chunks then blocks. */ +function makeFakeReader(chunks: Uint8Array[]) { + let i = 0; + let blockedResolve: ((v: { value?: Uint8Array; done: boolean }) => void) | undefined; + return { + read: vi.fn(async () => { + if (i < chunks.length) { + return { value: chunks[i++], done: false }; + } + // Block forever (simulating an open, idle stream) unless resolved externally. + return new Promise<{ value?: Uint8Array; done: boolean }>((resolve) => { + blockedResolve = resolve; + }); + }), + /** Test helper: end the stream, unblocking a pending read(). */ + finish() { + blockedResolve?.({ done: true }); + }, + }; +} + +function installFakeWebTransport(opts: { + writer: ReturnType; + reader: ReturnType; + closed?: Promise; +}) { + const ctorSpy = vi.fn(); + class FakeWebTransport { + ready = Promise.resolve(); + closed = opts.closed ?? new Promise(() => {}); + constructor(public url: string) { + ctorSpy(url); + } + createBidirectionalStream() { + return Promise.resolve({ + writable: { getWriter: () => opts.writer }, + readable: { getReader: () => opts.reader }, + }); + } + close = vi.fn(); + } + (globalThis as Record).WebTransport = FakeWebTransport; + return { ctorSpy, FakeWebTransport }; +} + +describe("WebTransportTransport", () => { + it("opens a session with the configured URL", async () => { + const writer = makeFakeWriter(); + const reader = makeFakeReader([]); + const { ctorSpy } = installFakeWebTransport({ writer, reader }); + + const transport = new WebTransportTransport("https://broker.example:9092"); + await transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + + expect(ctorSpy).toHaveBeenCalledWith("https://broker.example:9092"); + delete (globalThis as Record).WebTransport; + }); + + it("writes an auth frame to the bidi stream after stream creation when a token is set", async () => { + const writer = makeFakeWriter(); + const reader = makeFakeReader([]); + installFakeWebTransport({ writer, reader }); + + const transport = new WebTransportTransport("https://broker:9092", "tok-123"); + await transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + + expect(writer.writes).toHaveLength(1); + const decoded = new TextDecoder().decode(writer.writes[0]); + expect(JSON.parse(decoded)).toEqual({ type: "auth", token: "tok-123" }); + delete (globalThis as Record).WebTransport; + }); + + it("does not write an auth frame when no token is configured", async () => { + const writer = makeFakeWriter(); + const reader = makeFakeReader([]); + installFakeWebTransport({ writer, reader }); + + const transport = new WebTransportTransport("https://broker:9092"); + await transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + + expect(writer.writes).toHaveLength(0); + delete (globalThis as Record).WebTransport; + }); + + it("dispatches incoming bidi stream chunks via onFrame", async () => { + const writer = makeFakeWriter(); + const chunk = new Uint8Array([1, 2, 3]); + const reader = makeFakeReader([chunk]); + installFakeWebTransport({ writer, reader }); + + const onFrame = vi.fn(); + const transport = new WebTransportTransport("https://broker:9092"); + await transport.connect({ onFrame, onDisconnect: vi.fn() }); + + // Read loop runs in the background; flush microtasks. + await new Promise((r) => setTimeout(r, 0)); + + expect(onFrame).toHaveBeenCalledTimes(1); + expect(onFrame.mock.calls[0]?.[0]).toBe(chunk.buffer); + reader.finish(); + }); + + it("invokes onDisconnect when the session closes", async () => { + const writer = makeFakeWriter(); + const reader = makeFakeReader([]); + let resolveClosed: (() => void) | undefined; + const closedPromise = new Promise((resolve) => { + resolveClosed = resolve; + }); + installFakeWebTransport({ writer, reader, closed: closedPromise }); + + const onDisconnect = vi.fn(); + const transport = new WebTransportTransport("https://broker:9092"); + await transport.connect({ onFrame: vi.fn(), onDisconnect }); + + resolveClosed?.(); + await new Promise((r) => setTimeout(r, 0)); + + expect(onDisconnect).toHaveBeenCalledTimes(1); + reader.finish(); + }); + + it("invokes onDisconnect when the session's closed promise rejects", async () => { + const writer = makeFakeWriter(); + const reader = makeFakeReader([]); + let rejectClosed: ((e: unknown) => void) | undefined; + const closedPromise = new Promise((_resolve, reject) => { + rejectClosed = reject; + }); + installFakeWebTransport({ writer, reader, closed: closedPromise }); + + const onDisconnect = vi.fn(); + const transport = new WebTransportTransport("https://broker:9092"); + await transport.connect({ onFrame: vi.fn(), onDisconnect }); + + rejectClosed?.(new Error("session reset")); + await new Promise((r) => setTimeout(r, 0)); + + expect(onDisconnect).toHaveBeenCalledTimes(1); + reader.finish(); + }); + + it("send() writes bytes through the stream writer", async () => { + const writer = makeFakeWriter(); + const reader = makeFakeReader([]); + installFakeWebTransport({ writer, reader }); + + const transport = new WebTransportTransport("https://broker:9092"); + await transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + + const payload = new Uint8Array([9, 8, 7]).buffer; + await transport.send(payload); + + expect(writer.write).toHaveBeenLastCalledWith(new Uint8Array(payload)); + reader.finish(); + }); + + it("close() closes the writer then the session", async () => { + const writer = makeFakeWriter(); + const reader = makeFakeReader([]); + installFakeWebTransport({ writer, reader }); + + const transport = new WebTransportTransport("https://broker:9092"); + await transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }); + await transport.close(); + + expect(writer.close).toHaveBeenCalledTimes(1); + reader.finish(); + }); + + it("propagates connect() failure when the bidi stream cannot be created (no wrapping)", async () => { + class FailingWebTransport { + ready = Promise.resolve(); + closed = new Promise(() => {}); + constructor(public url: string) {} + createBidirectionalStream() { + return Promise.reject(new Error("stub: no bidi stream")); + } + close = vi.fn(); + } + (globalThis as Record).WebTransport = FailingWebTransport; + + const transport = new WebTransportTransport("https://broker:9092"); + await expect( + transport.connect({ onFrame: vi.fn(), onDisconnect: vi.fn() }), + ).rejects.toThrow("stub: no bidi stream"); + delete (globalThis as Record).WebTransport; + }); + + it("exposes kind = 'webtransport'", () => { + const transport = new WebTransportTransport("https://broker:9092"); + expect(transport.kind).toBe("webtransport"); + }); +}); diff --git a/src/transport.ts b/src/transport.ts new file mode 100644 index 0000000..4d8c2cb --- /dev/null +++ b/src/transport.ts @@ -0,0 +1,159 @@ +/** + * Internal transport implementations for {@link Client}. + * + * Both transports implement the small {@link Transport} contract so the + * client can select, connect, and delegate `send`/`close` without knowing + * which wire mechanism is in use. Reconnect policy, persistence, and topic + * dispatch remain the responsibility of {@link Client} — these units only + * own their respective connection's lifecycle framing. + */ +import { StreamlineError, StreamlineErrorCode } from "./types.js"; + +export type TransportKind = "webtransport" | "websocket"; + +/** Callbacks a {@link Transport} invokes on the owning {@link Client}. */ +export interface TransportCallbacks { + /** Invoked for each incoming frame (raw string or ArrayBuffer) from the broker. */ + onFrame: (data: ArrayBuffer | string) => void; + /** Invoked when the transport disconnects, intentionally or not. */ + onDisconnect: () => void; +} + +/** + * Contract shared by {@link WebSocketTransport} and {@link WebTransportTransport}. + */ +export interface Transport { + readonly kind: TransportKind; + /** Establish the connection and wire the given callbacks; resolves once ready to send. */ + connect(callbacks: TransportCallbacks): Promise; + /** Send a binary frame over this transport. */ + send(data: ArrayBuffer): Promise; + /** Close the transport. Safe to call even if already closed. */ + close(): Promise; +} + +// -------------------------------------------------------------------------- +// WebSocket transport +// -------------------------------------------------------------------------- + +export class WebSocketTransport implements Transport { + readonly kind: TransportKind = "websocket"; + + private socket?: WebSocket; + + constructor( + private readonly url: string, + private readonly token?: string, + ) {} + + connect(callbacks: TransportCallbacks): Promise { + return new Promise((resolve, reject) => { + const wsUrl = this.url.replace(/^http/, "ws"); + const sock = new WebSocket(wsUrl); + sock.binaryType = "arraybuffer"; + sock.onopen = () => { + if (this.token) { + sock.send(JSON.stringify({ type: "auth", token: this.token })); + } + this.socket = sock; + + sock.onmessage = (event: MessageEvent) => { + if (typeof event.data === "string" || event.data instanceof ArrayBuffer) { + callbacks.onFrame(event.data); + } + }; + sock.onclose = () => callbacks.onDisconnect(); + + resolve(); + }; + sock.onerror = (e) => reject(new StreamlineError( + `WebSocket connection failed: ${String(e)}`, + StreamlineErrorCode.Transport, + { retryable: true, hint: "Check that the Streamline server is running and accessible" }, + )); + }); + } + + send(data: ArrayBuffer): Promise { + this.socket?.send(data); + return Promise.resolve(); + } + + close(): Promise { + this.socket?.close(); + return Promise.resolve(); + } +} + +// -------------------------------------------------------------------------- +// WebTransport transport +// -------------------------------------------------------------------------- + +export class WebTransportTransport implements Transport { + readonly kind: TransportKind = "webtransport"; + + private conn?: WebTransport; + private writer?: WritableStreamDefaultWriter; + private reader?: ReadableStreamDefaultReader; + + constructor( + private readonly url: string, + private readonly token?: string, + ) {} + + /** + * Open a WebTransport session to the broker, create a bidirectional stream + * for the Streamline wire protocol, and send the auth token if configured. + */ + async connect(callbacks: TransportCallbacks): Promise { + const WT = (globalThis as unknown as { WebTransport: typeof WebTransport }).WebTransport; + const conn = new WT(this.url); + await conn.ready; + this.conn = conn; + + const bidi = await conn.createBidirectionalStream(); + this.writer = bidi.writable.getWriter(); + this.reader = bidi.readable.getReader(); + + if (this.token) { + const authFrame = new TextEncoder().encode( + JSON.stringify({ type: "auth", token: this.token }), + ); + await this.writer.write(authFrame); + } + + // Start background read loop for incoming records. + void this.readLoop(callbacks); + + // Auto-reconnect (via the client) when the session closes. + void conn.closed.then(() => callbacks.onDisconnect()).catch(() => callbacks.onDisconnect()); + } + + /** Continuously read from the WebTransport bidi stream and dispatch. */ + private async readLoop(callbacks: TransportCallbacks): Promise { + if (!this.reader) return; + try { + for (;;) { + const { value, done } = await this.reader.read(); + if (done) break; + callbacks.onFrame(value.buffer as ArrayBuffer); + } + } catch { + // Stream broken — handled via the closed-session callback above. + } + } + + async send(data: ArrayBuffer): Promise { + if (!this.writer) return; + await this.writer.write(new Uint8Array(data)); + } + + async close(): Promise { + try { + await this.writer?.close(); + } catch { /* already closed */ } + try { + this.conn?.close(); + } catch { /* already closed */ } + } +}