From 9c726af0b686d0ba85468c67724be68c9068f02e Mon Sep 17 00:00:00 2001 From: Kushagra Singh Bisen Date: Wed, 6 Mar 2024 14:59:47 +0100 Subject: [PATCH] adds:the first version of the Notifications Cache Server --- .gitignore | 1 - package-lock.json | 372 ++++++++++++++++++++-- package.json | 8 +- src/index.ts | 10 +- src/server/CacheServiceHTTPServer.ts | 84 ++++- src/service/CacheService.test.ts | 5 + src/service/CacheService.ts | 9 + src/service/SubscribeNotification.test.ts | 91 ++++++ src/service/SubscribeNotification.ts | 65 ++-- src/utils/Types.ts | 7 + src/utils/Util.test.ts | 111 +++++++ src/utils/Util.ts | 111 +++++++ 12 files changed, 805 insertions(+), 69 deletions(-) create mode 100644 src/utils/Types.ts diff --git a/.gitignore b/.gitignore index 6704566..a8c7599 100644 --- a/.gitignore +++ b/.gitignore @@ -69,7 +69,6 @@ typings/ .yarn-integrity # dotenv environment variables file -.env .env.test # parcel-bundler cache (https://parceljs.org/) diff --git a/package-lock.json b/package-lock.json index 87c85cd..b748a32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,14 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/n3": "^1.16.4", "@types/redis": "^4.0.11", "@typescript-eslint/parser": "^7.1.0", + "axios": "^1.6.7", "bunyan": "^1.8.15", + "commander": "^12.0.0", "ioredis": "^5.3.2", - "redis": "^4.6.13" + "n3": "^1.17.2" }, "devDependencies": { "@types/bunyan": "^1.8.11", @@ -1144,6 +1147,14 @@ "node": ">= 8" } }, + "node_modules/@rdfjs/types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-1.1.0.tgz", + "integrity": "sha512-5zm8bN2/CC634dTcn/0AhTRLaQRjXDZs3QfcAsQKNturHT7XVWcKy/8p3P5gXl+YkZTAmy7T5M/LyiT/jbkENw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -1320,11 +1331,19 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/n3": { + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.4.tgz", + "integrity": "sha512-6PmHRYCCdjbbBV2UVC/HjtL6/5Orx9ku2CQjuojucuHvNvPmnm6+02B18YGhHfvU25qmX2jPXyYPHsMNkn+w2w==", + "dependencies": { + "@rdfjs/types": "^1.1.0", + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.11.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1662,6 +1681,17 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -1781,8 +1811,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "29.3.1", @@ -1880,6 +1919,25 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1949,6 +2007,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2127,7 +2208,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2135,6 +2215,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "engines": { + "node": ">=18" + } + }, "node_modules/comment-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", @@ -2220,7 +2308,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -2664,6 +2751,22 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2829,11 +2932,29 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3069,6 +3190,25 @@ "node": ">=10.17.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -3968,9 +4108,9 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -4148,7 +4288,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -4157,7 +4296,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -4262,6 +4400,18 @@ "rimraf": "bin.js" } }, + "node_modules/n3": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.17.2.tgz", + "integrity": "sha512-BxSM52wYFqXrbQQT5WUEzKUn6qpYV+2L4XZLfn3Gblz2kwZ09S+QxC33WNdVEQy2djenFL8SNkrjejEKlvI6+Q==", + "dependencies": { + "queue-microtask": "^1.1.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/nan": { "version": "2.18.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", @@ -4552,6 +4702,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -4565,6 +4723,11 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4613,6 +4776,21 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/redis": { "version": "4.6.13", "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.13.tgz", @@ -4746,6 +4924,25 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-json-stringify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", @@ -4878,6 +5075,14 @@ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -5199,8 +5404,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/update-browserslist-db": { "version": "1.0.10", @@ -6213,6 +6417,14 @@ "fastq": "^1.6.0" } }, + "@rdfjs/types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-1.1.0.tgz", + "integrity": "sha512-5zm8bN2/CC634dTcn/0AhTRLaQRjXDZs3QfcAsQKNturHT7XVWcKy/8p3P5gXl+YkZTAmy7T5M/LyiT/jbkENw==", + "requires": { + "@types/node": "*" + } + }, "@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -6376,11 +6588,19 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "@types/n3": { + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.4.tgz", + "integrity": "sha512-6PmHRYCCdjbbBV2UVC/HjtL6/5Orx9ku2CQjuojucuHvNvPmnm6+02B18YGhHfvU25qmX2jPXyYPHsMNkn+w2w==", + "requires": { + "@rdfjs/types": "^1.1.0", + "@types/node": "*" + } + }, "@types/node": { "version": "20.11.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", - "dev": true, "requires": { "undici-types": "~5.26.4" } @@ -6602,6 +6822,14 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -6685,8 +6913,17 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "requires": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "babel-jest": { "version": "29.3.1", @@ -6763,6 +7000,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6810,6 +7052,15 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6932,11 +7183,15 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==" + }, "comment-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", @@ -7004,8 +7259,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "denque": { "version": "2.1.0", @@ -7300,6 +7554,16 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7437,11 +7701,15 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, + "follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7607,6 +7875,11 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -8284,9 +8557,9 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "keyv": { @@ -8421,14 +8694,12 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "requires": { "mime-db": "1.52.0" } @@ -8508,6 +8779,15 @@ } } }, + "n3": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.17.2.tgz", + "integrity": "sha512-BxSM52wYFqXrbQQT5WUEzKUn6qpYV+2L4XZLfn3Gblz2kwZ09S+QxC33WNdVEQy2djenFL8SNkrjejEKlvI6+Q==", + "requires": { + "queue-microtask": "^1.1.2", + "readable-stream": "^4.0.0" + } + }, "nan": { "version": "2.18.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", @@ -8718,6 +8998,11 @@ } } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8728,6 +9013,11 @@ "sisteransi": "^1.0.5" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8753,6 +9043,18 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, "redis": { "version": "4.6.13", "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.13.tgz", @@ -8838,6 +9140,11 @@ "queue-microtask": "^1.2.2" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, "safe-json-stringify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", @@ -8949,6 +9256,14 @@ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -9160,8 +9475,7 @@ "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "update-browserslist-db": { "version": "1.0.10", diff --git a/package.json b/package.json index 0389aa2..d6d2fa7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "scripts": { "build": "npx tsc", - "start": "node dist/index.js", + "start": "npx tsc && node --max-old-space-size=8192 dist/index.js cache-notifications", "test": "jest --coverage", "lint:ts": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", "lint:ts:fix": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0 --fix", @@ -27,9 +27,13 @@ "typescript": "^4.9.4" }, "dependencies": { + "@types/n3": "^1.16.4", "@types/redis": "^4.0.11", "@typescript-eslint/parser": "^7.1.0", + "axios": "^1.6.7", "bunyan": "^1.8.15", - "ioredis": "^5.3.2" + "commander": "^12.0.0", + "ioredis": "^5.3.2", + "n3": "^1.17.2" } } diff --git a/src/index.ts b/src/index.ts index 1fb98c4..e1110c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { CacheServiceHTTPServer } from "./server/CacheServiceHTTPServer"; import * as bunyan from "bunyan"; import * as fs from 'fs'; -const program = require('commander'); +import { program } from "commander"; const log_file = fs.createWriteStream('./logs/info.log', { flags: 'a' }); const logger = bunyan.createLogger({ @@ -30,7 +30,9 @@ program .command('cache-notifications') .description('Starts the cache service for notifications from the LDES stream stored in the solid server(s).') .option('-p, --port ', 'The port where the HTTP server will listen.', '8085') - .option('-l --ldes ', 'The location of the LDES Stream', 'http://localhost:3000/aggregation_pod/aggregation/') + .option('-l --ldes ', 'The location of the LDES Stream', ['http://localhost:3000/aggregation_pod/aggregation/']) .action((options: any) => { - new CacheServiceHTTPServer(options.port, options.pod, logger); - }); \ No newline at end of file + new CacheServiceHTTPServer(options.port, options.ldes, logger); + }); + +program.parse(process.argv); \ No newline at end of file diff --git a/src/server/CacheServiceHTTPServer.ts b/src/server/CacheServiceHTTPServer.ts index ad96462..29905d7 100644 --- a/src/server/CacheServiceHTTPServer.ts +++ b/src/server/CacheServiceHTTPServer.ts @@ -1,5 +1,7 @@ import * as http from 'http'; +import * as url from 'url'; import { CacheService } from '../service/CacheService'; +import { SubscribeNotification } from '../service/SubscribeNotification'; /** * A class for the HTTP server that interacts with the cache service to handle requests. @@ -9,19 +11,24 @@ import { CacheService } from '../service/CacheService'; export class CacheServiceHTTPServer { private readonly cacheService: CacheService; private readonly server: http.Server; - private pod_url: string; + public logger: any; + private subscription_notification: SubscribeNotification; + private pod_url_array: string[]; + /** * Creates an instance of CacheServiceHTTPServer. * @param {number} port - The port where the HTTP server will listen. - * @param {string} pod_url - The location of the Solid Pod from which the notifications are retrieved. + * @param {string[]} pod_url - The location of the Solid Pod from which the notifications are retrieved. * @param {*} logger - The logger object. * @memberof CacheServiceHTTPServer */ - constructor(port: number, pod_url: string, logger: any) { + constructor(port: number, pod_url: string[], logger: any) { + this.logger = logger; this.cacheService = new CacheService(); - this.pod_url = pod_url; + this.pod_url_array = pod_url; + this.subscription_notification = new SubscribeNotification(this.pod_url_array); this.server = http.createServer(this.request_handler.bind(this)); - this.setupServer(port); + this.setupServerAndSubscribe(port); } /** * Sets up the HTTP server where it listens on the specified port as well as connects to the cache service. @@ -29,11 +36,21 @@ export class CacheServiceHTTPServer { * @param {number} port - The port where the HTTP server will listen. * @memberof CacheServiceHTTPServer */ - private async setupServer(port: number) { - await this.cacheService.connect(); - this.server.listen(port, () => { - console.log(`Server listening on port ${port}`); - }); + private async setupServerAndSubscribe(port: number) { + if (await this.cacheService.get_status() === "connecting") { + const subscription_successful = await this.subscription_notification.subscribe(); + if(subscription_successful) { + console.log(`Subscription was successful`); + + } + this.server.listen(port, () => { + this.logger.info(`Server listening on port ${port}`); + }); + } + else { + this.logger.error("Cache service is not connecting"); + await this.cacheService.connect(); + } } /** * Handles the requests to the HTTP server. @@ -43,7 +60,7 @@ export class CacheServiceHTTPServer { * @returns {Promise} - A promise which responses nothing. * @memberof CacheServiceHTTPServer */ - private async request_handler(request: http.IncomingMessage, response: http.ServerResponse) { + public async request_handler(request: http.IncomingMessage, response: http.ServerResponse) { if (request.method === 'POST') { await this.handleNotificationPostRequest(request, response); } @@ -67,7 +84,35 @@ export class CacheServiceHTTPServer { * @memberof CacheServiceHTTPServer */ private async handleNotificationPostRequest(request: http.IncomingMessage, response: http.ServerResponse): Promise { - + let body = ''; + request.on('data', (chunk) => { + body += chunk.toString(); + }); + request.on('end', async () => { + try { + const notification = JSON.parse(body); + const published = (new Date(notification.published).getTime()).toString(); + const resource_location = notification.object; + try { + const resource_fetch_response = await fetch(resource_location, { + method: 'GET', + headers: { + 'Accept': 'text/turtle' + } + }); + this.logger.info("Resource fetched successfully"); + const response_text = await resource_fetch_response.text(); + this.cacheService.set(published, response_text); + } catch (error) { + this.logger.error("Error fetching the resource: " + error); + } + response.writeHead(200, 'OK'); + response.end('OK'); + } catch (error) { + response.writeHead(400, 'Bad Request'); + response.end('Bad Request'); + } + }); } /** * Handles the GET requests to the HTTP server, which are requests from the clients to retrieve the notifications. @@ -78,7 +123,13 @@ export class CacheServiceHTTPServer { * @memberof CacheServiceHTTPServer */ private async handleClientGetRequest(request: http.IncomingMessage, response: http.ServerResponse): Promise { - + this.logger.info(`GET request received for ${request.url}`) + console.log(`GET request received for ${request.url}`); + const parsed_url = url.parse(request.url!, true); + const query_parameters = parsed_url.query; + const event_time = query_parameters.event_time as string | undefined || 'Anonymous'; + response.writeHead(200, 'OK', { 'Content-Type': 'text/turtle' }); + response.end(await this.cacheService.get(event_time)); } /** * Handles the DELETE requests to the HTTP server. @@ -89,7 +140,12 @@ export class CacheServiceHTTPServer { * @memberof CacheServiceHTTPServer */ private async handleNotificationDeleteRequest(request: http.IncomingMessage, response: http.ServerResponse): Promise { - + const parsed_url = url.parse(request.url!, true); + const query_parameters = parsed_url.query; + const event_time = query_parameters.event_time as string | undefined || 'Anonymous'; + await this.cacheService.delete(event_time); + response.writeHead(200, 'OK'); + response.end('OK'); } diff --git a/src/service/CacheService.test.ts b/src/service/CacheService.test.ts index 17d7112..f2798cf 100644 --- a/src/service/CacheService.test.ts +++ b/src/service/CacheService.test.ts @@ -28,4 +28,9 @@ describe('CacheService', () => { const is_disconnected = await cacheService.disconnect(); expect(is_disconnected).toBe(true); }); + + it('should_describe_the_cache', async() => { + const status = await cacheService.get_status(); + expect(status).toBe('wait'); + }) }); \ No newline at end of file diff --git a/src/service/CacheService.ts b/src/service/CacheService.ts index dd8d8d8..460d116 100644 --- a/src/service/CacheService.ts +++ b/src/service/CacheService.ts @@ -1,4 +1,5 @@ import { Redis } from "ioredis"; +import { RedisStatus } from "../utils/Types"; /** * A service for interacting with the Redis cache. * @class CacheService @@ -79,5 +80,13 @@ export class CacheService { async delete(key: string) { await this.client.del(key); } + /** + * Get the status of the Redis cache. + * @returns {Promise} - The current status of the Redis Cache, returned as a promise. + * @memberof CacheService + */ + async get_status(): Promise { + return await this.client.status; + } } diff --git a/src/service/SubscribeNotification.test.ts b/src/service/SubscribeNotification.test.ts index e69de29..df49970 100644 --- a/src/service/SubscribeNotification.test.ts +++ b/src/service/SubscribeNotification.test.ts @@ -0,0 +1,91 @@ +import { SubscribeNotification } from './SubscribeNotification'; +import { extract_ldp_inbox, extract_subscription_server } from '../utils/Util'; + +jest.mock('../utils/Util', () => ({ + extract_ldp_inbox: jest.fn(), + extract_subscription_server: jest.fn(), +})); + +describe('SubscribeNotification', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should subscribe successfully', async () => { + const streams = ['stream1', 'stream2']; + const subscribeNotification = new SubscribeNotification(streams); + + (extract_ldp_inbox as jest.Mock).mockResolvedValueOnce('inbox1'); + (extract_subscription_server as jest.Mock).mockResolvedValueOnce({ + location: 'http://subscription-server1', + }); + + const fetchMock = jest.fn().mockResolvedValueOnce({ status: 200 }); + global.fetch = fetchMock; + + const result = await subscribeNotification.subscribe(); + + expect(result).toBe(true); + expect(extract_ldp_inbox).toHaveBeenCalledWith('stream1'); + expect(extract_subscription_server).toHaveBeenCalledWith('inbox1'); + expect(fetchMock).toHaveBeenCalledWith('http://subscription-server1', { + method: 'POST', + headers: { + 'Content-Type': 'application/ld+json', + }, + body: JSON.stringify({ + '@context': ['https://www.w3.org/ns/solid/notification/v1'], + type: 'http://www.w3.org/ns/solid/notifications#WebhookChannel2023', + topic: 'inbox1', + sendTo: 'http://localhost:8085/', + }), + }); + }); + + it('should throw an error if subscription server is undefined', async () => { + const streams = ['stream1']; + const subscribeNotification = new SubscribeNotification(streams); + + (extract_ldp_inbox as jest.Mock).mockResolvedValueOnce('inbox1'); + (extract_subscription_server as jest.Mock).mockResolvedValueOnce(undefined); + + await expect(subscribeNotification.subscribe()).rejects.toThrow( + 'Subscription server is undefined.' + ); + + expect(extract_ldp_inbox).toHaveBeenCalledWith('stream1'); + expect(extract_subscription_server).toHaveBeenCalledWith('inbox1'); + }); + + it('should throw an error if subscription to the notification server fails', async () => { + const streams = ['stream1']; + const subscribeNotification = new SubscribeNotification(streams); + + (extract_ldp_inbox as jest.Mock).mockResolvedValueOnce('inbox1'); + (extract_subscription_server as jest.Mock).mockResolvedValueOnce({ + location: 'http://subscription-server1', + }); + + const fetchMock = jest.fn().mockResolvedValueOnce({ status: 500 }); + global.fetch = fetchMock; + + await expect(subscribeNotification.subscribe()).rejects.toThrow( + 'The subscription to the notification server failed.' + ); + + expect(extract_ldp_inbox).toHaveBeenCalledWith('stream1'); + expect(extract_subscription_server).toHaveBeenCalledWith('inbox1'); + expect(fetchMock).toHaveBeenCalledWith('http://subscription-server1', { + method: 'POST', + headers: { + 'Content-Type': 'application/ld+json', + }, + body: JSON.stringify({ + '@context': ['https://www.w3.org/ns/solid/notification/v1'], + type: 'http://www.w3.org/ns/solid/notifications#WebhookChannel2023', + topic: 'inbox1', + sendTo: 'http://localhost:8085/', + }), + }); + }); +}); diff --git a/src/service/SubscribeNotification.ts b/src/service/SubscribeNotification.ts index e67f325..9f0f3ab 100644 --- a/src/service/SubscribeNotification.ts +++ b/src/service/SubscribeNotification.ts @@ -1,24 +1,51 @@ -export class SubscribeNotification { - public solid_pod_urls: string[]; +import { extract_ldp_inbox, extract_subscription_server } from "../utils/Util"; - constructor(solid_pod_urls: string[]) { - this.solid_pod_urls = solid_pod_urls; +/** + * This class is used to subscribe to the notification server for real-time notifications. + * @class SubscribeNotification + */ +export class SubscribeNotification { + private ldes_streams: string[]; + /** + * Creates an instance of SubscribeNotification. + * @param {string[]} streams - An array of LDES streams to subscribe to, for real-time notifications. + * @memberof SubscribeNotification + */ + constructor(streams: string[]) { + this.ldes_streams = streams; } - public async subscribe() { - console.log(`Subscribing to notifications for ${this.solid_pod_urls}...`); - + /** + * Subscribes to the notification server for each LDES stream in the constructor, using the inbox and subscription server. + * @returns {(Promise)} - Returns a promise with a boolean or undefined. If the subscription is successful, it returns true. If the subscription fails, it throws an error. + * @memberof SubscribeNotification + */ + public async subscribe(): Promise { + for (const stream of this.ldes_streams) { + const inbox = await extract_ldp_inbox(stream) as string; + const subscription_server = await extract_subscription_server(inbox); + if (subscription_server === undefined) { + throw new Error("Subscription server is undefined."); + } else { + const response = await fetch(subscription_server.location, { + method: 'POST', + headers: { + 'Content-Type': 'application/ld+json' + }, + body: JSON.stringify({ + "@context": ["https://www.w3.org/ns/solid/notification/v1"], + "type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023", + "topic": inbox, + "sendTo": "http://localhost:8085/" + }) + }); + if (response.status === 200) { + return true; + } + else { + throw new Error("The subscription to the notification server failed."); + } + } + } } - -} - - -/** - * sending the server `http://localhost:3000/.notifications/WebhookChannel2023/` - { - "@context": [ "https://www.w3.org/ns/solid/notification/v1" ], - "type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023", - "topic": "http://localhost:3000/aggregation_pod/", - "sendTo": "http://localhost:3001/" } -*/ \ No newline at end of file diff --git a/src/utils/Types.ts b/src/utils/Types.ts new file mode 100644 index 0000000..d4d05c5 --- /dev/null +++ b/src/utils/Types.ts @@ -0,0 +1,7 @@ +export type SubscriptionServerNotification = { + location: string, + channelType: string, + channelLocation: string +} + +export type RedisStatus = "wait" | "reconnecting" | "connecting" | "connect" | "ready" | "close" | "end"; diff --git a/src/utils/Util.test.ts b/src/utils/Util.test.ts index e69de29..b6091ca 100644 --- a/src/utils/Util.test.ts +++ b/src/utils/Util.test.ts @@ -0,0 +1,111 @@ +import axios from 'axios'; +import { create_subscription, extract_ldp_inbox, extract_subscription_server } from './Util'; + +jest.mock('axios'); +describe('Util_Functions', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should_extract_subscription_server_from_resource', async () => { + const mockHeadResponse = { + headers: { + vary: 'Accept,Authorization,Origin', + 'x-powered-by': 'Community Solid Server', + 'access-control-allow-origin': '*', + 'access-control-allow-credentials': 'true', + 'access-control-expose-headers': 'Accept-Patch,Accept-Post,Accept-Put,Allow,Content-Range,ETag,Last-Modified,Link,Location,Updates-Via,WAC-Allow,Www-Authenticate', + allow: 'OPTIONS, HEAD, GET, PATCH, PUT, DELETE', + 'accept-patch': 'text/n3, application/sparql-update', + 'accept-put': '*/*', + 'content-type': 'text/turtle', + link: '; rel="type", ; rel="describedby", ; rel="http://www.w3.org/ns/solid/terms#storageDescription"', + 'last-modified': 'Tue, 27 Feb 2024 11:47:34 GMT', + etag: '"1709034454000-text/turtle"', + 'content-length': '645928', + date: 'Tue, 05 Mar 2024 12:41:48 GMT', + connection: 'keep-alive', + 'keep-alive': 'timeout=5' + } + }; + + const mockGetResponse = { + data: ` + a ; + . + ; + , , , , . + ` + }; + + (axios.head as jest.Mock).mockResolvedValue(mockHeadResponse); + (axios.get as jest.Mock).mockResolvedValue(mockGetResponse); + jest.spyOn(console, 'error').mockImplementation(() => { }); + + const result = await extract_subscription_server('http://localhost:3000/aggregation_pod/'); + expect(axios.head).toHaveBeenCalledWith('http://localhost:3000/aggregation_pod/'); + expect(axios.get).toHaveBeenCalledWith('http://localhost:3000/aggregation_pod/.well-known/solid'); + expect(console.error).not.toHaveBeenCalled(); + expect(result).toEqual({ + location: 'http://localhost:3000/.notifications/WebhookChannel2023/', + channelType: 'http://www.w3.org/ns/solid/notifications#WebhookChannel2023', + channelLocation: 'http://localhost:3000/.notifications/WebhookChannel2023/' + }); + }); + + it('should_throw_an_error_if_there_is_an_error_while_extracting_subscription_server', async () => { + (axios.head as jest.Mock).mockRejectedValue(new Error('Network error')); + jest.spyOn(console, 'error').mockImplementation(() => { }); + + await expect(extract_subscription_server('http://example.com/resource')).rejects.toThrowError( + 'Error while extracting subscription server.' + ); + + expect(axios.head).toHaveBeenCalledWith('http://example.com/resource'); + expect(console.error).toHaveBeenCalled(); + }); + + it('should_extract_ldp_inbox_from_ldes_stream_location', async () => { + const mockResponse = { + text: jest.fn().mockResolvedValue(` + @prefix ldp: . + ldp:inbox . + `) + }; + + jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse as any); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const result = await extract_ldp_inbox('http://example.com/resource'); + + expect(global.fetch).toHaveBeenCalledWith('http://example.com/resource'); + expect(console.error).not.toHaveBeenCalled(); + expect(result).toBe('http://example.com/resourceinbox'); + }); + it('should_create_subscription', async () => { + const mockSubscriptionServer = 'http://example.com/subscription'; + const mockInboxLocation = 'http://example.com/inbox'; + const mockResponse = { + text: jest.fn().mockResolvedValue('Subscription created successfully.') + }; + jest.spyOn(global, 'fetch').mockResolvedValue(mockResponse as any); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const result = await create_subscription(mockSubscriptionServer, mockInboxLocation); + + expect(global.fetch).toHaveBeenCalledWith(mockSubscriptionServer, { + method: 'POST', + headers: { + 'Content-Type': 'application/ld+json' + }, + body: JSON.stringify({ + "@context": ["https://www.w3.org/ns/solid/notification/v1"], + "type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023", + "topic": `${mockInboxLocation}`, + "sendTo": "http://localhost:8085/" + }) + }); + expect(console.error).not.toHaveBeenCalled(); + expect(result).toBe('Subscription created successfully.'); + }); +}); \ No newline at end of file diff --git a/src/utils/Util.ts b/src/utils/Util.ts index e69de29..d4d90a7 100644 --- a/src/utils/Util.ts +++ b/src/utils/Util.ts @@ -0,0 +1,111 @@ +import axios from 'axios'; +import { SubscriptionServerNotification } from './Types'; +const N3 = require('n3'); +const parser = new N3.Parser(); +const store = new N3.Store(); + +/** + * Extracts the subscription server from the given resource. + * @param {string} resource - The resource which you want to read the notifications from. + * @returns {Promise} - A promise which returns the subscription server or if not returns undefined. + */ +export async function extract_subscription_server(resource: string): Promise { + try { + const response = await axios.head(resource); + const link_header = response.headers['link']; + if (link_header) { + const link_header_parts = link_header.split(','); + for (const part of link_header_parts) { + const [link, rel] = part.split(';').map((item: string) => item.trim()); + if (rel === 'rel="http://www.w3.org/ns/solid/terms#storageDescription"') { + const storage_description_link = link.slice(1, -1); // remove the < and >\ + const storage_description_response = await axios.get(storage_description_link); + const storage_description = storage_description_response.data; + await parser.parse(storage_description, (error: any, quad: any) => { + if (quad) { + store.addQuad(quad); + } + }); + const subscription_server = store.getQuads(null, 'http://www.w3.org/ns/solid/notifications#subscription', null)[0].object.value; + const subscription_type = store.getQuads(null, 'http://www.w3.org/ns/solid/notifications#channelType', null)[0].object.value; + const channelLocation = store.getQuads(null, 'http://www.w3.org/ns/solid/notifications#channelType', null)[0].subject.value; + const subscription_response: SubscriptionServerNotification = { + location: subscription_server, + channelType: subscription_type, + channelLocation: channelLocation + } + return subscription_response; + } + else { + continue; + } + } + } + } catch (error) { + console.error(error); + throw new Error("Error while extracting subscription server."); + } +} + +/** + * Extracts the inbox location from the given LDES stream location. + * @param {string} ldes_stream_location - The location of the LDES stream. + * @returns {Promise} - A promise which returns the inbox location. + */ +export async function extract_ldp_inbox(ldes_stream_location: string) { + try { + const response = await fetch(ldes_stream_location); + if (response) { + await parser.parse(await response.text(), (error: any, quad: any) => { + if (error) { + console.error(error); + throw new Error("Error while parsing LDES stream."); + } + if (quad) { + store.addQuad(quad); + } + }); + const inbox = store.getQuads(null, 'http://www.w3.org/ns/ldp#inbox', null)[0].object.value; + return ldes_stream_location + inbox; + } + else { + throw new Error("The response object is empty."); + } + } catch (error) { + console.error(error); + } +} + + +/** + * Creates a subscription to the Caching Service's HTTP Server for the given inbox location to read the notifications. + * @param {string} subscription_server - The subscription server (of the Solid Server) where the subscription will be created. + * @param {string} inbox_location - The location of the inbox where the notifications are written by the client(s). + * @returns {Promise} - A promise which returns the response text. + */ +export async function create_subscription(subscription_server: string, inbox_location: string) { + try { + const subscription = { + "@context": ["https://www.w3.org/ns/solid/notification/v1"], + "type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023", + "topic": `${inbox_location}`, + "sendTo": "http://localhost:8085/" + } + const response = await fetch(subscription_server, { + method: 'POST', + headers: { + 'Content-Type': 'application/ld+json' + }, + body: JSON.stringify(subscription) + }) + if (response) { + return response.text(); + } + else { + console.error("The response object is empty."); + throw new Error("The response object is empty."); + } + } catch (error) { + throw new Error("Error while creating subscription."); + } +} \ No newline at end of file