From 81a9abd13a68a3797c9fe47488f210f24fe78847 Mon Sep 17 00:00:00 2001 From: Sasha Date: Mon, 29 Sep 2025 23:41:02 +0200 Subject: [PATCH] chore: add waku/react package --- package-lock.json | 493 ++++++++++++++++++++- package.json | 3 +- packages/react/.mocharc.cjs | 26 ++ packages/react/CHANGELOG.md | 11 + packages/react/package.json | 102 +++++ packages/react/rollup.config.js | 99 +++++ packages/react/src/ContentPairProvider.tsx | 53 +++ packages/react/src/WakuProvider.tsx | 64 +++ packages/react/src/index.ts | 8 + packages/react/src/types.ts | 31 ++ packages/react/src/useCreatContentPair.ts | 59 +++ packages/react/src/useCreateWaku.ts | 65 +++ packages/react/src/useFilterMessages.ts | 89 ++++ packages/react/src/useLightPush.ts | 44 ++ packages/react/src/useStoreMessages.ts | 101 +++++ packages/react/tsconfig.dev.json | 3 + packages/react/tsconfig.json | 11 + packages/react/typedoc.json | 4 + 18 files changed, 1264 insertions(+), 2 deletions(-) create mode 100644 packages/react/.mocharc.cjs create mode 100644 packages/react/CHANGELOG.md create mode 100644 packages/react/package.json create mode 100644 packages/react/rollup.config.js create mode 100644 packages/react/src/ContentPairProvider.tsx create mode 100644 packages/react/src/WakuProvider.tsx create mode 100644 packages/react/src/index.ts create mode 100644 packages/react/src/types.ts create mode 100644 packages/react/src/useCreatContentPair.ts create mode 100644 packages/react/src/useCreateWaku.ts create mode 100644 packages/react/src/useFilterMessages.ts create mode 100644 packages/react/src/useLightPush.ts create mode 100644 packages/react/src/useStoreMessages.ts create mode 100644 packages/react/tsconfig.dev.json create mode 100644 packages/react/tsconfig.json create mode 100644 packages/react/typedoc.json diff --git a/package-lock.json b/package-lock.json index fa49a49a1b..bbf3c8b0c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "packages/reliability-tests", "packages/headless-tests", "packages/browser-tests", - "packages/build-utils" + "packages/build-utils", + "packages/react" ], "devDependencies": { "@size-limit/preset-big-lib": "^11.0.2", @@ -5841,6 +5842,29 @@ } } }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", @@ -7626,6 +7650,13 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -7640,6 +7671,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "18.3.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -8385,6 +8427,10 @@ "resolved": "packages/proto", "link": true }, + "node_modules/@waku/react": { + "resolved": "packages/react", + "link": true + }, "node_modules/@waku/relay": { "resolved": "packages/relay", "link": true @@ -13643,6 +13689,13 @@ "node": ">=18" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, "node_modules/custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", @@ -16086,6 +16139,19 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -16146,6 +16212,16 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz", + "integrity": "sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -29794,6 +29870,19 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -31068,6 +31157,53 @@ "node": ">= 4.0.0" } }, + "node_modules/rollup-plugin-typescript2": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.36.0.tgz", + "integrity": "sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^4.1.2", + "find-cache-dir": "^3.3.2", + "fs-extra": "^10.0.0", + "semver": "^7.5.4", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "rollup": ">=1.26.3", + "typescript": ">=2.4.0" + } + }, + "node_modules/rollup-plugin-typescript2/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/rollup-plugin-typescript2/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -32719,6 +32855,13 @@ "npm": ">= 3.0.0" } }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -37373,6 +37516,354 @@ "node": ">=22" } }, + "packages/react": { + "name": "@waku/react", + "version": "0.0.7", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@waku/interfaces": "0.0.34", + "@waku/sdk": "0.0.35", + "@waku/utils": "0.0.27" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "@types/chai": "^4.3.11", + "@types/mocha": "^10.0.9", + "@types/react": "^18.0.28", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "@waku/build-utils": "*", + "chai": "^5.1.1", + "cspell": "^8.6.1", + "eslint": "^8.34.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-simple-import-sort": "^10.0.0", + "mocha": "^10.7.3", + "npm-run-all": "^4.1.5", + "rollup": "^4.12.0", + "rollup-plugin-typescript2": "^0.36.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "packages/react/node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/react/node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/react/node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/react/node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/react/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/react/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/react/node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "packages/react/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/react/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "packages/react/node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/react/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "packages/react/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "packages/react/node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "packages/react/node_modules/eslint-plugin-prettier": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.5.tgz", + "integrity": "sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "packages/react/node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "packages/react/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/react/node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "packages/relay": { "name": "@waku/relay", "version": "0.0.22", diff --git a/package.json b/package.json index b4551f0b68..36a199dc3c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "packages/reliability-tests", "packages/headless-tests", "packages/browser-tests", - "packages/build-utils" + "packages/build-utils", + "packages/react" ], "scripts": { "prepare": "husky", diff --git a/packages/react/.mocharc.cjs b/packages/react/.mocharc.cjs new file mode 100644 index 0000000000..a2d47774f0 --- /dev/null +++ b/packages/react/.mocharc.cjs @@ -0,0 +1,26 @@ +const config = { + extension: ['ts', 'tsx'], + spec: 'src/**/*.spec.{ts,tsx}', + require: ['ts-node/register', 'isomorphic-fetch'], + loader: 'ts-node/esm', + 'node-option': [ + 'experimental-specifier-resolution=node', + 'loader=ts-node/esm' + ], + exit: true, + retries: 4 +}; + +if (process.env.CI) { + console.log("Running tests in parallel"); + config.parallel = true; + config.jobs = 6; + console.log("Activating allure reporting"); + config.reporter = 'mocha-multi-reporters'; + config.reporterOptions = { + configFile: '.mocha.reporters.json' + }; +} else { + console.log("Running tests serially. To enable parallel execution update mocha config"); +} +module.exports = config; diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md new file mode 100644 index 0000000000..9572b7e333 --- /dev/null +++ b/packages/react/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.7] - Unreleased + +### Added +- Initial release with React hooks and components for js-waku diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 0000000000..1b043adfba --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,102 @@ +{ + "name": "@waku/react", + "version": "0.0.7", + "description": "React hooks and components to use js-waku", + "type": "module", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.mjs", + "umd:main": "dist/index.umd.js", + "unpkg": "dist/index.umd.js", + "source": "src/index.ts", + "types": "dist/index.d.ts", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.esm.mjs", + "require": "./dist/index.cjs.js" + } + }, + "author": "Waku Team", + "homepage": "https://github.com/waku-org/js-waku/tree/master/packages/react#readme", + "repository": { + "type": "git", + "url": "https://github.com/waku-org/js-waku.git" + }, + "bugs": { + "url": "https://github.com/waku-org/js-waku/issues" + }, + "license": "MIT OR Apache-2.0", + "keywords": [ + "waku", + "decentralized", + "secure", + "communication", + "web3", + "ethereum", + "dapps", + "privacy", + "react" + ], + "scripts": { + "build": "rollup --config rollup.config.js", + "fix": "run-s fix:*", + "fix:lint": "eslint src *.js --fix", + "check": "run-s check:*", + "check:tsc": "tsc -p tsconfig.dev.json", + "check:lint": "eslint src *.js", + "check:spelling": "cspell \"{README.md,src/**/*.ts,src/**/*.tsx}\"", + "test": "NODE_ENV=test run-s test:*", + "test:node": "NODE_ENV=test TS_NODE_PROJECT=./tsconfig.dev.json mocha", + "watch:build": "tsc -p tsconfig.json -w", + "watch:test": "mocha --watch", + "prepublish": "npm run build", + "reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build" + }, + "engines": { + "node": ">=22" + }, + "dependencies": { + "@waku/interfaces": "0.0.34", + "@waku/sdk": "0.0.35", + "@waku/utils": "0.0.27" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@types/chai": "^4.3.11", + "@types/mocha": "^10.0.9", + "@types/react": "^18.0.28", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "@waku/build-utils": "*", + "chai": "^5.1.1", + "cspell": "^8.6.1", + "eslint": "^8.34.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-simple-import-sort": "^10.0.0", + "mocha": "^10.7.3", + "npm-run-all": "^4.1.5", + "rollup": "^4.12.0", + "@rollup/plugin-terser": "^0.4.4", + "rollup-plugin-typescript2": "^0.36.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + }, + "files": [ + "dist", + "bundle", + "src/**/*.ts", + "src/**/*.tsx", + "!**/*.spec.*", + "!**/*.json", + "CHANGELOG.md", + "LICENSE", + "README.md" + ] +} diff --git a/packages/react/rollup.config.js b/packages/react/rollup.config.js new file mode 100644 index 0000000000..fae00960ce --- /dev/null +++ b/packages/react/rollup.config.js @@ -0,0 +1,99 @@ +import commonjs from "@rollup/plugin-commonjs"; +import json from "@rollup/plugin-json"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import terser from "@rollup/plugin-terser"; +import typescript from "rollup-plugin-typescript2"; + +const input = "src/index.ts"; + +const external = [ + "react", + "react-dom", + "@waku/interfaces", + "@waku/sdk", + "@waku/utils" +]; + +export default [ + { + input, + output: { + file: "dist/index.esm.mjs", + format: "esm", + sourcemap: true + }, + external, + plugins: [ + nodeResolve({ + browser: true, + preferBuiltins: false + }), + commonjs(), + json(), + typescript({ + tsconfig: "./tsconfig.json", + useTsconfigDeclarationDir: true + }) + ] + }, + { + input, + output: { + file: "dist/index.cjs.js", + format: "cjs", + sourcemap: true, + exports: "named" + }, + external, + plugins: [ + nodeResolve({ + browser: true, + preferBuiltins: false + }), + commonjs(), + json(), + typescript({ + tsconfig: "./tsconfig.json", + tsconfigOverride: { + compilerOptions: { + declaration: false + } + } + }) + ] + }, + { + input, + output: { + file: "dist/index.umd.js", + format: "umd", + name: "WakuReact", + sourcemap: true, + globals: { + react: "React", + "react-dom": "ReactDOM", + "@waku/interfaces": "WakuInterfaces", + "@waku/sdk": "WakuSDK", + "@waku/utils": "WakuUtils" + } + }, + external, + plugins: [ + nodeResolve({ + browser: true, + preferBuiltins: false + }), + commonjs(), + json(), + typescript({ + tsconfig: "./tsconfig.json", + tsconfigOverride: { + compilerOptions: { + declaration: false + } + } + }), + terser() + ] + } +]; diff --git a/packages/react/src/ContentPairProvider.tsx b/packages/react/src/ContentPairProvider.tsx new file mode 100644 index 0000000000..877d153270 --- /dev/null +++ b/packages/react/src/ContentPairProvider.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +import type { ContentPair, ReactChildrenProps } from "./types.js"; +import { useCreateContentPair } from "./useCreatContentPair.js"; + +type ContentPairContextType = Partial; + +const ContentPairContext = React.createContext({ + decoder: undefined, + encoder: undefined +}); + +/** + * Hook to retrieve Encoder/Decoder pair from Context. + * @example + * const { encoder, decoder } = useContentPair(); + * @returns {Object} { encoder, decoder } + */ +export const useContentPair = (): ContentPairContextType => + React.useContext(ContentPairContext); + +type ContentPairProviderProps = ReactChildrenProps & { + contentTopic: string; + ephemeral?: boolean; +}; + +/** + * Provider for creating Encoder/Decoder pair based on contentTopic + * @example + * const App = (props) => ( + * + * + * + * ); + * const Component = (props) => { + * const { encoder, decoder } = useContentPair(); + * ... + * }; + * @param {string} contentTopic - content topic for configuring the pair + * @param {boolean} ephemeral - flag to set messages ephemeral according to RFC https://rfc.vac.dev/spec/14/ + * @returns React ContentPair Provider component + */ +export const ContentPairProvider: React.FunctionComponent< + ContentPairProviderProps +> = (props) => { + const result = useCreateContentPair(props.contentTopic, props.ephemeral); + + return ( + + {props.children} + + ); +}; diff --git a/packages/react/src/WakuProvider.tsx b/packages/react/src/WakuProvider.tsx new file mode 100644 index 0000000000..e09ba1dedc --- /dev/null +++ b/packages/react/src/WakuProvider.tsx @@ -0,0 +1,64 @@ +import type { CreateNodeOptions, IWaku } from "@waku/interfaces"; +import React from "react"; + +import type { + BootstrapNodeOptions, + CreateNodeResult, + ReactChildrenProps +} from "./types.js"; +import { useCreateLightNode } from "./useCreateWaku.js"; + +type WakuContextType = CreateNodeResult; + +const WakuContext = React.createContext>({ + node: undefined, + isLoading: false, + error: undefined +}); + +/** + * Hook to retrieve Waku node from Context. By default generic Waku type will be used. + * @example + * const { node, isLoading, error } = useWaku(); + * @example + * const { node, isLoading, error } = useWaku(); + * @example + * const { node, isLoading, error } = useWaku(); + * @example + * const { node, isLoading, error } = useWaku(); + * @returns WakuContext + */ +export const useWaku = (): WakuContextType => + React.useContext(WakuContext) as WakuContextType; + +type ProviderProps = ReactChildrenProps & BootstrapNodeOptions; + +/** + * Provider for creating Light Node based on options passed. + * @example + * const App = (props) => ( + * + * + * + * ); + * const Component = (props) => { + * const { node, isLoading, error } = useWaku(); + * ... + * }; + * @param {Object} props - options to create a node and other React props + * @param {CreateNodeOptions} props.options - optional options for creating Light Node + * @param {Protocols} props.protocols - optional protocols list to initiate node with + * @returns React Light Node provider component + */ +export const LightNodeProvider: React.FunctionComponent< + ProviderProps +> = (props) => { + const result = useCreateLightNode({ + options: props.options, + protocols: props.protocols + }); + + return ( + {props.children} + ); +}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 0000000000..8450cfdf1a --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1,8 @@ +export { ContentPairProvider, useContentPair } from "./ContentPairProvider.js"; +export { CreateNodeOptions } from "./types.js"; +export { useCreateContentPair } from "./useCreatContentPair.js"; +export { useCreateLightNode } from "./useCreateWaku.js"; +export { useFilterMessages } from "./useFilterMessages.js"; +export { useLightPush } from "./useLightPush.js"; +export { useStoreMessages } from "./useStoreMessages.js"; +export { LightNodeProvider, useWaku } from "./WakuProvider.js"; diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts new file mode 100644 index 0000000000..e8f1006112 --- /dev/null +++ b/packages/react/src/types.ts @@ -0,0 +1,31 @@ +import type { + IDecodedMessage, + IDecoder, + IEncoder, + IWaku, + Protocols +} from "@waku/interfaces"; +export type { CreateNodeOptions, AutoSharding } from "@waku/interfaces"; + +export type HookState = { + isLoading: boolean; + error: undefined | string; +}; + +export type CreateNodeResult = HookState & { + node: undefined | T; +}; + +export type BootstrapNodeOptions> = { + options?: T; + protocols?: Protocols[]; +}; + +export type ContentPair = { + encoder: IEncoder; + decoder: IDecoder; +}; + +export type ReactChildrenProps = { + children?: React.ReactNode; +}; diff --git a/packages/react/src/useCreatContentPair.ts b/packages/react/src/useCreatContentPair.ts new file mode 100644 index 0000000000..be426b8b30 --- /dev/null +++ b/packages/react/src/useCreatContentPair.ts @@ -0,0 +1,59 @@ +import type { + AutoSharding, + IDecodedMessage, + IDecoder, + IEncoder +} from "@waku/interfaces"; +import { createDecoder, createEncoder } from "@waku/sdk"; +import { createRoutingInfo } from "@waku/utils"; +import React from "react"; + +import type { ContentPair } from "./types.js"; + +const defaultNetworkConfig: AutoSharding = { + clusterId: 0, + numShardsInCluster: 8 +}; + +/** + * Creates Encoder / Decoder pair for a given contentTopic. + * @param {string} contentTopic - topic to orient to + * @param {boolean} ephemeral - makes messages ephemeral, default to false + * @param {AutoSharding} networkConfig - optional network config, defaults to cluster 0 with 8 shards + * @returns {Object} Encoder / Decoder pair + */ +export const useCreateContentPair = ( + contentTopic: string, + ephemeral = false, + networkConfig: AutoSharding = defaultNetworkConfig +): ContentPair => { + const routingInfo = React.useMemo( + () => createRoutingInfo(networkConfig, { contentTopic }), + [contentTopic, networkConfig.clusterId, networkConfig.numShardsInCluster] + ); + + const [encoder, setEncoder] = React.useState( + createEncoder({ contentTopic, ephemeral, routingInfo }) + ); + const [decoder, setDecoder] = React.useState>( + createDecoder(contentTopic, routingInfo) + ); + + React.useEffect(() => { + const newRoutingInfo = createRoutingInfo(networkConfig, { contentTopic }); + setEncoder( + createEncoder({ contentTopic, ephemeral, routingInfo: newRoutingInfo }) + ); + setDecoder(createDecoder(contentTopic, newRoutingInfo)); + }, [ + contentTopic, + ephemeral, + networkConfig.clusterId, + networkConfig.numShardsInCluster + ]); + + return { + encoder, + decoder + }; +}; diff --git a/packages/react/src/useCreateWaku.ts b/packages/react/src/useCreateWaku.ts new file mode 100644 index 0000000000..7ad96bb5b0 --- /dev/null +++ b/packages/react/src/useCreateWaku.ts @@ -0,0 +1,65 @@ +import type { CreateNodeOptions, IWaku, LightNode } from "@waku/interfaces"; +import { createLightNode, waitForRemotePeer } from "@waku/sdk"; +import React from "react"; + +import type { BootstrapNodeOptions, CreateNodeResult } from "./types.js"; + +type NodeFactory> = (options?: T) => Promise; + +type CreateNodeParams< + N extends IWaku, + T = Record +> = BootstrapNodeOptions & { + factory: NodeFactory; +}; + +const useCreateNode = >( + params: CreateNodeParams +): CreateNodeResult => { + const { factory, options, protocols = [] } = params; + + const [node, setNode] = React.useState(undefined); + const [isLoading, setLoading] = React.useState(true); + const [error, setError] = React.useState(undefined); + + React.useEffect(() => { + let cancelled = false; + setLoading(true); + + factory(options) + .then(async (node) => { + if (cancelled) { + return; + } + + await node.start(); + await waitForRemotePeer(node, protocols); + + setNode(node); + setLoading(false); + }) + .catch((err) => { + setLoading(false); + setError(`Failed at creating node: ${err?.message || "no message"}`); + }); + + return () => { + cancelled = true; + }; + }, []); + + return { + node, + error, + isLoading + }; +}; + +export const useCreateLightNode = ( + params?: BootstrapNodeOptions +): CreateNodeResult => { + return useCreateNode({ + ...params, + factory: createLightNode + }); +}; diff --git a/packages/react/src/useFilterMessages.ts b/packages/react/src/useFilterMessages.ts new file mode 100644 index 0000000000..9a7f7bf6a7 --- /dev/null +++ b/packages/react/src/useFilterMessages.ts @@ -0,0 +1,89 @@ +import type { + IDecodedMessage, + IDecoder, + IFilter, + IWaku +} from "@waku/interfaces"; +import { Logger } from "@waku/utils"; +import React from "react"; + +import type { HookState } from "./types.js"; + +const log = new Logger("react:useFilterMessages"); + +type AbstractFilterNode = IWaku & { + filter: IFilter; +}; + +type UseFilterMessagesParams = { + node: undefined | AbstractFilterNode; + decoder: undefined | IDecoder; +}; + +type UseFilterMessagesResult = HookState & { + messages: IDecodedMessage[]; +}; + +/** + * Returns messages from Filter subscription and keeps them up to date + * @example + * const { isLoading, error, message } = useFilterMessages({node, decoder}); + * @param {Object} node - node that implements Filter, hook does nothing if undefined + * @param {Object} decoder - decoder to use for subscribing, hook does nothing if undefined + * @returns {Object} hook state (isLoading, error) and messages array + */ +export const useFilterMessages = ( + params: UseFilterMessagesParams +): UseFilterMessagesResult => { + const { node, decoder } = params; + + const [error, setError] = React.useState(undefined); + const [isLoading, setLoading] = React.useState(false); + const [messages, setMessage] = React.useState([]); + + const pushMessage = React.useCallback( + (message: IDecodedMessage): void => { + if (!message) { + return; + } + + setMessage((prev) => [...prev, message]); + }, + [setMessage] + ); + + React.useEffect(() => { + if (!node || !decoder) { + return; + } + + setLoading(true); + + node.filter + .subscribe([decoder], pushMessage) + .then((success) => { + setLoading(false); + if (!success) { + setError("Failed to subscribe to filter"); + } + }) + .catch((err) => { + setLoading(false); + setError( + `Failed to subscribe to filter: ${err?.message || "no message"}` + ); + }); + + return () => { + node.filter.unsubscribe([decoder]).catch((err) => { + log.error("Failed to unsubscribe:", err); + }); + }; + }, [node, decoder, pushMessage, setError, setLoading]); + + return { + error, + messages, + isLoading + }; +}; diff --git a/packages/react/src/useLightPush.ts b/packages/react/src/useLightPush.ts new file mode 100644 index 0000000000..10c2f2f5e3 --- /dev/null +++ b/packages/react/src/useLightPush.ts @@ -0,0 +1,44 @@ +import type { + IEncoder, + ILightPush, + IMessage, + IWaku, + LightPushSDKResult +} from "@waku/interfaces"; +import React from "react"; + +type AbstractLightPushNode = IWaku & { + lightPush: ILightPush; +}; + +type UseLightPushParams = { + encoder: undefined | IEncoder; + node: undefined | AbstractLightPushNode; +}; + +type PushFn = (message: IMessage) => Promise; + +type UseLightPushResult = { + push?: undefined | PushFn; +}; + +export const useLightPush = ( + params: UseLightPushParams +): UseLightPushResult => { + const { node, encoder } = params; + + const push = React.useCallback( + (message) => { + return node!.lightPush.send(encoder as IEncoder, message); + }, + [node, encoder] + ); + + if (!node && !encoder) { + return {}; + } + + return { + push + }; +}; diff --git a/packages/react/src/useStoreMessages.ts b/packages/react/src/useStoreMessages.ts new file mode 100644 index 0000000000..2d0aade7c7 --- /dev/null +++ b/packages/react/src/useStoreMessages.ts @@ -0,0 +1,101 @@ +import type { + IDecodedMessage, + IDecoder, + IStore, + IWaku, + QueryRequestParams +} from "@waku/interfaces"; +import React from "react"; + +import type { HookState } from "./types.js"; + +type AbstractStoreNode = IWaku & { + store: IStore; +}; + +type UseStoreMessagesParams = { + node: undefined | AbstractStoreNode; + decoder: undefined | IDecoder; + options: QueryRequestParams; +}; + +type UseStoreMessagesResult = HookState & { + messages: IDecodedMessage[]; +}; + +/** + * Hook for retrieving messages from Store protocol based on options + * @example + * const { isLoading, error, messages } = useStoreMessages({node, decoder, options}); + * @param {Object} node - node that implement Store, hook does nothing if undefined + * @param {Object} decoder - decoder to use for getting messages, hook does nothing if undefined + * @param {QueryRequestParams} options - options to initiate query to get messages + * @returns {Object} hook state (isLoading, error) and messages array + */ +export const useStoreMessages = ( + params: UseStoreMessagesParams +): UseStoreMessagesResult => { + const { node, decoder, options } = params; + + const [error, setError] = React.useState(undefined); + const [isLoading, setLoading] = React.useState(false); + const [messages, setMessage] = React.useState([]); + + const pushMessage = React.useCallback( + (newMessages: IDecodedMessage[]): void => { + if (!newMessages || !newMessages.length) { + return; + } + + setMessage((prev) => [...prev, ...newMessages]); + }, + [setMessage] + ); + + React.useEffect(() => { + if (!node || !decoder) { + return; + } + + let cancelled = false; + setLoading(true); + + Promise.resolve() + .then(async () => { + for await (const promises of node.store.queryGenerator( + [decoder], + options + )) { + if (cancelled) { + return; + } + + const messagesRaw = await Promise.all(promises); + const filteredMessages = messagesRaw.filter( + (v): v is IDecodedMessage => !!v + ); + + pushMessage(filteredMessages); + } + + setLoading(false); + }) + .catch((err) => { + setLoading(false); + setError( + `Failed to query messages from store: ${err?.message || "no message"}` + ); + }); + + return () => { + cancelled = true; + }; + // TODO: missing dependency on options, it will prevent consecutive update if options change + }, [node, decoder, pushMessage, setError, setLoading]); + + return { + error, + isLoading, + messages + }; +}; diff --git a/packages/react/tsconfig.dev.json b/packages/react/tsconfig.dev.json new file mode 100644 index 0000000000..4f7c34af3c --- /dev/null +++ b/packages/react/tsconfig.dev.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.dev" +} diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 0000000000..aa507d3e52 --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "dist/", + "rootDir": "src", + "tsBuildInfoFile": "dist/.tsbuildinfo", + "jsx": "react" + }, + "include": ["src", "*.js"], + "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "src/test_utils"] +} diff --git a/packages/react/typedoc.json b/packages/react/typedoc.json new file mode 100644 index 0000000000..f593f276c2 --- /dev/null +++ b/packages/react/typedoc.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": ["src/index.ts"] +}