diff --git a/examples/nodejs/token-vending-machine/infrastructure/package-lock.json b/examples/nodejs/token-vending-machine/infrastructure/package-lock.json index a13040003..3c812eb83 100644 --- a/examples/nodejs/token-vending-machine/infrastructure/package-lock.json +++ b/examples/nodejs/token-vending-machine/infrastructure/package-lock.json @@ -8,7 +8,7 @@ "name": "infrastructure", "version": "0.1.0", "dependencies": { - "aws-cdk-lib": "^2.85.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0", "source-map-support": "^0.5.21" }, @@ -19,7 +19,7 @@ "@types/jest": "^29.4.0", "@types/node": "18.11.18", "@typescript-eslint/eslint-plugin": "5.59.11", - "aws-cdk": "2.85.0", + "aws-cdk": "2.158.0", "eslint": "8.42.0", "eslint-config-prettier": "8.8.0", "eslint-plugin-import": "2.27.5", @@ -55,19 +55,54 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.200", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.200.tgz", - "integrity": "sha512-Kf5J8DfJK4wZFWT2Myca0lhwke7LwHcHBo+4TvWOGJrFVVKVuuiLCkzPPRBQQVDj0Vtn2NBokZAz8pfMpAqAKg==" + "version": "2.2.202", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", + "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==" }, "node_modules/@aws-cdk/asset-kubectl-v20": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==" }, - "node_modules/@aws-cdk/asset-node-proxy-agent-v5": { - "version": "2.0.166", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.166.tgz", - "integrity": "sha512-j0xnccpUQHXJKPgCwQcGGNu4lRiC1PptYfdxBIH1L4dRK91iBxtSQHESRQX+yB47oGLaF/WfNN/aF3WXwlhikg==" + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "36.0.25", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-36.0.25.tgz", + "integrity": "sha512-AK86v4IMV4zcWfp392e3wlaVJPT72/dk39Lo2SDDFxQR+sikMOyY2IGrULyhK1TwQmPiyxM7QB/0MkTbMDAPrw==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dependencies": { + "jsonschema": "^1.4.1", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.18.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.6.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, "node_modules/@babel/code-frame": { "version": "7.22.13", @@ -2079,9 +2114,9 @@ } }, "node_modules/aws-cdk": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.85.0.tgz", - "integrity": "sha512-duRE5rvP9Qu5iUNgA6+knHKsQ7xI6yKMUxyARTveYEzW/qDHD0RWKRu+pDbbwXLlzcr25oKGPjC3dM0ui2beKg==", + "version": "2.158.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.158.0.tgz", + "integrity": "sha512-UcrxBG02RACrnTvfuyZiTuOz8gqOpnqjCMTdVmdpExv5qk9hddhtRAubNaC4xleHuNJnvskYqqVW+Y3Abh6zGQ==", "dev": true, "bin": { "cdk": "bin/cdk" @@ -2094,9 +2129,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.88.0.tgz", - "integrity": "sha512-bmhokh30HVeqlotWaoEmK7mKB9SJbJwpbsiVgmYe3JcMu8DposHQqaIPI7LnC+dg015tZaxUsExxOYBEw+vntQ==", + "version": "2.158.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.158.0.tgz", + "integrity": "sha512-Pl9CCLM+XRTy6nyyRJM1INEMtwIlZOib0FWyq9i9E388vurw7sNVJ6tAsfLpGIOLHsFQCbF4f6OZ0KSVxmMaiA==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -2107,21 +2142,24 @@ "punycode", "semver", "table", - "yaml" + "yaml", + "mime-types" ], "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.200", + "@aws-cdk/asset-awscli-v1": "^2.2.202", "@aws-cdk/asset-kubectl-v20": "^2.1.2", - "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.165", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^36.0.24", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.1.1", - "ignore": "^5.2.4", + "fs-extra": "^11.2.0", + "ignore": "^5.3.2", "jsonschema": "^1.4.1", + "mime-types": "^2.1.35", "minimatch": "^3.1.2", - "punycode": "^2.3.0", - "semver": "^7.5.4", - "table": "^6.8.1", + "punycode": "^2.3.1", + "semver": "^7.6.3", + "table": "^6.8.2", "yaml": "1.10.2" }, "engines": { @@ -2137,14 +2175,14 @@ "license": "Apache-2.0" }, "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.12.0", + "version": "8.17.1", "inBundle": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2234,8 +2272,13 @@ "inBundle": true, "license": "MIT" }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT" + }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.1.1", + "version": "11.2.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -2253,7 +2296,7 @@ "license": "ISC" }, "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.2.4", + "version": "5.3.2", "inBundle": true, "license": "MIT", "engines": { @@ -2297,15 +2340,23 @@ "inBundle": true, "license": "MIT" }, - "node_modules/aws-cdk-lib/node_modules/lru-cache": { - "version": "6.0.0", + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", "inBundle": true, - "license": "ISC", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, "node_modules/aws-cdk-lib/node_modules/minimatch": { @@ -2320,7 +2371,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/punycode": { - "version": "2.3.0", + "version": "2.3.1", "inBundle": true, "license": "MIT", "engines": { @@ -2336,12 +2387,9 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.5.4", + "version": "7.6.3", "inBundle": true, "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -2390,7 +2438,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.8.1", + "version": "6.8.2", "inBundle": true, "license": "BSD-3-Clause", "dependencies": { @@ -2405,26 +2453,13 @@ } }, "node_modules/aws-cdk-lib/node_modules/universalify": { - "version": "2.0.0", + "version": "2.0.1", "inBundle": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, - "node_modules/aws-cdk-lib/node_modules/uri-js": { - "version": "4.4.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/yallist": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, "node_modules/aws-cdk-lib/node_modules/yaml": { "version": "1.10.2", "inBundle": true, @@ -3975,7 +4010,6 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, "engines": { "node": ">= 4" } @@ -5622,7 +5656,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, "engines": { "node": ">=6" } @@ -5847,7 +5880,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -5862,7 +5894,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -5873,8 +5904,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/shebang-command": { "version": "2.0.0", diff --git a/examples/nodejs/token-vending-machine/infrastructure/package.json b/examples/nodejs/token-vending-machine/infrastructure/package.json index fbe7f1b11..25360ffa5 100644 --- a/examples/nodejs/token-vending-machine/infrastructure/package.json +++ b/examples/nodejs/token-vending-machine/infrastructure/package.json @@ -18,7 +18,7 @@ "@types/jest": "^29.4.0", "@types/node": "18.11.18", "@typescript-eslint/eslint-plugin": "5.59.11", - "aws-cdk": "2.85.0", + "aws-cdk": "2.158.0", "eslint": "8.42.0", "eslint-config-prettier": "8.8.0", "eslint-plugin-import": "2.27.5", @@ -31,7 +31,7 @@ "typescript": "~4.9.4" }, "dependencies": { - "aws-cdk-lib": "^2.85.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0", "source-map-support": "^0.5.21" } diff --git a/examples/nodejs/token-vending-machine/lambda/token-vending-machine/handler.ts b/examples/nodejs/token-vending-machine/lambda/token-vending-machine/handler.ts index 66c289894..20540bcbb 100644 --- a/examples/nodejs/token-vending-machine/lambda/token-vending-machine/handler.ts +++ b/examples/nodejs/token-vending-machine/lambda/token-vending-machine/handler.ts @@ -36,6 +36,9 @@ export const handler = async (event: APIGatewayProxyEvent): Promise npm run dictionary Example Code: [dictionary.ts](dictionary.ts) +## Running the TopicClient example with auto-refreshing disposable tokens + +This example implements TokenRefreshingTopicClient, an example wrapper class around the TopicClient that refreshes disposable tokens before they expire. Getting a new disposable token requires creating a new TopicClient that accepts a CredentialProvider that uses the new token. After the new TopicClient is created, existing subscriptions must be transferred to the new client. All of this occurs within the TokenRefreshingTopicClient. + +If you run the example using the `localTokenVendingMachine()` method passed to the wrapped client (this is the default for this example): + +```typescript + const wrappedTopicClient = await TokenRefreshingTopicClient.create({ + refreshBeforeExpiryMs: 10_000, // 10 seconds before token expires, refresh it. + getDisposableToken: localTokenVendingMachine, + }); +``` + +Run the example using: + +```bash +# Run example code +MOMENTO_API_KEY= npm run tokens +``` + +If you have deployed a token vending machine to generate disposable tokens like so: + +```typescript + const wrappedTopicClient = await TokenRefreshingTopicClient.create({ + refreshBeforeExpiryMs: 10_000, // 10 seconds before token expires, refresh it. + getDisposableToken: tokenVendingMachine, + }); +``` + +Run the example using: + +```bash +# Run example code +TVM_ENDPOINT= npm run tokens +``` + +Example Code: [refresh-disposable-tokens.ts](refresh-disposable-tokens.ts) + If you have questions or need help experimenting further, please reach out to us! {{ ossFooter }} diff --git a/examples/web/cache/package-lock.json b/examples/web/cache/package-lock.json index 40a30d25c..3ade0a547 100644 --- a/examples/web/cache/package-lock.json +++ b/examples/web/cache/package-lock.json @@ -13,7 +13,7 @@ "jsdom": "22.1.0" }, "devDependencies": { - "@types/node": "16.11.4", + "@types/node": "22.5.4", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.0.0", "eslint": "8.19.0", @@ -199,10 +199,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.11.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.4.tgz", - "integrity": "sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ==", - "dev": true + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } }, "node_modules/@types/semver": { "version": "7.5.8", @@ -3248,6 +3251,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -3562,10 +3571,13 @@ "dev": true }, "@types/node": { - "version": "16.11.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.4.tgz", - "integrity": "sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ==", - "dev": true + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dev": true, + "requires": { + "undici-types": "~6.19.2" + } }, "@types/semver": { "version": "7.5.8", @@ -5699,6 +5711,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", diff --git a/examples/web/cache/package.json b/examples/web/cache/package.json index e38dba606..129edf820 100644 --- a/examples/web/cache/package.json +++ b/examples/web/cache/package.json @@ -9,7 +9,8 @@ "example": "tsc && node dist/basic.js", "advanced": "tsc && node dist/advanced.js", "dictionary": "tsc && node dist/dictionary.js", - "validate-examples": "tsc && node dist/basic.js && node dist/advanced.js && node dist/dictionary.js && node dist/readme.js", + "tokens": "tsc && node dist/refresh-disposable-tokens.js", + "validate-examples": "tsc && node dist/basic.js && node dist/advanced.js && node dist/dictionary.js && node dist/readme.js && node dist/refresh-disposable-tokens.js", "test": "jest", "lint": "eslint . --ext .ts", "format": "eslint . --ext .ts --fix" @@ -17,7 +18,7 @@ "author": "", "license": "ISC", "devDependencies": { - "@types/node": "16.11.4", + "@types/node": "22.5.4", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.0.0", "eslint": "8.19.0", diff --git a/examples/web/cache/refresh-disposable-tokens.ts b/examples/web/cache/refresh-disposable-tokens.ts new file mode 100644 index 000000000..e62001ff0 --- /dev/null +++ b/examples/web/cache/refresh-disposable-tokens.ts @@ -0,0 +1,250 @@ +import { + AllTopics, + AuthClient, + CredentialProvider, + DisposableTokenScopes, + ExpiresAt, + ExpiresIn, + GenerateDisposableTokenResponse, + TopicClient, + TopicItem, + TopicPublish, + TopicPublishResponse, + TopicSubscribe, + TopicSubscribeResponse, +} from '@gomomento/sdk-web'; +import {initJSDom} from './utils/jsdom'; + +// In your own setup, the token vending machine would likely be a separate service. +// In this file, we provide a local version if you want to run this example completely independently. +// We also provide a version that fetches a token from a deployed token vending machine. +// See https://github.com/momentohq/client-sdk-javascript/tree/main/examples/nodejs/token-vending-machine +// for more information about deploying your own token vending machine. + +async function localTokenVendingMachine(): Promise<{token: string; expiresAt: ExpiresAt}> { + const authClient = new AuthClient({}); + const tokenResponse = await authClient.generateDisposableToken( + DisposableTokenScopes.topicPublishSubscribe('my-cache', AllTopics), + ExpiresIn.seconds(30) + ); + if (tokenResponse.type === GenerateDisposableTokenResponse.Error) { + throw new Error(`Failed to generate a disposable token: ${tokenResponse.toString()}`); + } + const humanReadableTime = new Date(tokenResponse.expiresAt.epoch() * 1000).toISOString(); + console.log(`Generated a disposable token that will expire at ${humanReadableTime}`); + const disposableToken = { + token: tokenResponse.authToken, + expiresAt: tokenResponse.expiresAt, + }; + return disposableToken; +} + +async function tokenVendingMachine(): Promise<{token: string; expiresAt: ExpiresAt}> { + const resp = await fetch(process.env.TVM_ENDPOINT as string); + const respJson = (await resp.json()) as {authToken: string; expiresAt: number}; + const disposableToken = { + token: respJson.authToken, + expiresAt: ExpiresAt.fromEpoch(respJson.expiresAt), + }; + return disposableToken; +} + +interface TokenRefreshingTopicClientProps { + refreshBeforeExpiryMs: number; + getDisposableToken: () => Promise<{token: string; expiresAt: ExpiresAt}>; +} + +class TokenRefreshingTopicClient { + topicClient: TopicClient; + refreshBeforeExpiryMs: number; + getDisposableToken: () => Promise<{token: string; expiresAt: ExpiresAt}>; + activeSubscriptions: Record< + string, + { + cacheName: string; + topicName: string; + lastSequenceNumber: number; + unsubscribe: () => void; + onItem: (item: TopicItem) => void; + onError: (error: TopicSubscribe.Error) => void; + } + > = {}; + + private constructor(props: TokenRefreshingTopicClientProps) { + this.refreshBeforeExpiryMs = props.refreshBeforeExpiryMs; + this.getDisposableToken = props.getDisposableToken; + } + + private async initialize() { + const disposableToken = await this.getDisposableToken(); + this.topicClient = new TopicClient({ + credentialProvider: CredentialProvider.fromString(disposableToken.token), + }); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + setTimeout(async () => { + await this.refreshToken(); + }, getRefreshAfterMs(disposableToken.expiresAt, this.refreshBeforeExpiryMs)); + console.log('Initialized topic client and set first timeout'); + } + + static async create(props: TokenRefreshingTopicClientProps) { + const client = new TokenRefreshingTopicClient(props); + await client.initialize(); + return client; + } + + private async refreshToken() { + console.log('Disposable token expiring soon, refreshing topic client with new token'); + const disposableToken = await this.getDisposableToken(); + const newTopicClient = new TopicClient({ + credentialProvider: CredentialProvider.fromString(disposableToken.token), + }); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + setTimeout(async () => { + await this.refreshToken(); + }, getRefreshAfterMs(disposableToken.expiresAt, this.refreshBeforeExpiryMs)); + + // for each active subscription, unsubscribe from the old topic client and subscribe to the new one + for (const key in this.activeSubscriptions) { + const value = this.activeSubscriptions[key]; + const newSubscription = await newTopicClient.subscribe(value.cacheName, value.topicName, { + onItem: value.onItem, + onError: value.onError, + }); + value.unsubscribe(); + + if (newSubscription.type === TopicSubscribeResponse.Error) { + console.error(`Error subscribing to topic: ${newSubscription.toString()}`); + } else { + this.activeSubscriptions[key].unsubscribe = () => { + newSubscription.unsubscribe(); + }; + } + } + this.topicClient = newTopicClient; + } + + async publish(cacheName: string, topicName: string, message: string, onError?: (resp: TopicPublish.Error) => void) { + const resp = await this.topicClient.publish(cacheName, topicName, message); + if (resp.type === TopicPublishResponse.Error) { + console.error(`Error publishing message: ${resp.toString()}`); + if (onError) { + onError(resp); + } + } + } + + async subscribe( + cacheName: string, + topicName: string, + options: { + onItem: (item: TopicItem) => void; + onError: (error: TopicSubscribe.Error) => void; + } + ) { + console.log(`Subscribe function called for ${cacheName}:${topicName}`); + + const wrappedOnItem = (item: TopicItem) => { + const currentSubscription = this.activeSubscriptions[`${cacheName}:${topicName}`]; + // pass through to user-provided onItem only if message hasn't been processed before + if (item.sequenceNumber() > currentSubscription.lastSequenceNumber) { + options.onItem(item); + currentSubscription.lastSequenceNumber = item.sequenceNumber(); + } + }; + + const resp = await this.topicClient.subscribe(cacheName, topicName, { + onItem: wrappedOnItem, + onError: options.onError, + }); + + if (resp.type === TopicSubscribeResponse.Error) { + console.error(`Error subscribing to topic: ${resp.toString()}`); + } else { + console.log(`Subscribed to ${cacheName}:${topicName}`); + const key = `${cacheName}:${topicName}`; + + // if key already exists, update the existing subscription + // otherwise make new record + if (key in this.activeSubscriptions) { + this.activeSubscriptions[key].unsubscribe = () => { + resp.unsubscribe(); + }; + } else { + this.activeSubscriptions[key] = { + cacheName, + topicName, + lastSequenceNumber: 0, + unsubscribe: () => { + resp.unsubscribe(); + }, + onItem: wrappedOnItem, + onError: options.onError, + }; + } + console.log('New subscription added, active subscriptions are now:', this.activeSubscriptions); + } + } +} + +function getRefreshAfterMs(expiresAt: ExpiresAt, refreshBefore: number): number { + const refreshingIn = expiresAt.epoch() * 1000 - Date.now() - refreshBefore; + console.log(`Refreshing in ${refreshingIn} ms`); + return refreshingIn; +} + +const main = async () => { + // Because the Momento Web SDK is intended for use in a browser, we use the JSDom library + // to set up an environment that will allow us to use it in a node.js program. + initJSDom(); + + // Expecting uninterrupted sequence number progression from each subscription. + const onItemA = (item: TopicItem) => { + console.log(`Callback A: User code processing message ${item.sequenceNumber()}`); + }; + const onItemB = (item: TopicItem) => { + console.log(`Callback B: User code processing message ${item.sequenceNumber()}`); + }; + const onError = (error: TopicSubscribe.Error) => { + console.error(`User code received error: ${error.toString()}`); + }; + const onPublishError = (resp: TopicPublish.Error) => { + console.error(`User code received error while publishing message: ${resp.toString()}`); + }; + + const wrappedTopicClient = await TokenRefreshingTopicClient.create({ + refreshBeforeExpiryMs: 10_000, // 10 seconds before token expires, refresh it. + getDisposableToken: localTokenVendingMachine, + }); + + await wrappedTopicClient.subscribe('my-cache', 'topic-1', { + onItem: onItemA, + onError, + }); + + await wrappedTopicClient.subscribe('my-cache', 'topic-2', { + onItem: onItemB, + onError, + }); + + const endDemoTime = Date.now() + 45_000; // Run for 45 seconds + + // Meanwhile, publish messages and see how the topic client + // is refreshed after tokens expire every 20 seconds. + while (Date.now() < endDemoTime) { + await wrappedTopicClient.publish('my-cache', 'topic-1', 'Message for topic 1', onPublishError); + await wrappedTopicClient.publish('my-cache', 'topic-2', 'Message for topic 2', onPublishError); + } +}; + +main() + .then(() => { + console.log('End of demo!'); + // Don't leave the process hanging + // eslint-disable-next-line no-process-exit + process.exit(0); + }) + .catch((e: Error) => { + console.error(`Uncaught exception while running disposable tokens example: ${e.message}`); + throw e; + }); diff --git a/examples/web/vite-chat-app/.eslintrc.cjs b/examples/web/vite-chat-app/.eslintrc.cjs index ba8fd3370..ea10e2354 100644 --- a/examples/web/vite-chat-app/.eslintrc.cjs +++ b/examples/web/vite-chat-app/.eslintrc.cjs @@ -13,7 +13,6 @@ module.exports = { parserOptions: { ecmaVersion: 'latest', sourceType: 'module', - project: true, tsconfigRootDir: __dirname, }, plugins: ['react-refresh'], diff --git a/examples/web/vite-chat-app/.gitignore b/examples/web/vite-chat-app/.gitignore index a547bf36d..7ceb59f89 100644 --- a/examples/web/vite-chat-app/.gitignore +++ b/examples/web/vite-chat-app/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.env diff --git a/examples/web/vite-chat-app/index.html b/examples/web/vite-chat-app/index.html index e0d1c8408..ff7fb7c05 100644 --- a/examples/web/vite-chat-app/index.html +++ b/examples/web/vite-chat-app/index.html @@ -2,9 +2,9 @@ - + - Vite + React + TS + Topics Chat App Demo
diff --git a/examples/web/vite-chat-app/public/favicon.ico b/examples/web/vite-chat-app/public/favicon.ico new file mode 100644 index 000000000..b7087a2ee Binary files /dev/null and b/examples/web/vite-chat-app/public/favicon.ico differ diff --git a/examples/web/vite-chat-app/public/vite.svg b/examples/web/vite-chat-app/public/vite.svg deleted file mode 100644 index e7b8dfb1b..000000000 --- a/examples/web/vite-chat-app/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/web/vite-chat-app/src/App.tsx b/examples/web/vite-chat-app/src/App.tsx index 2ffcc2b5d..23da68a9a 100644 --- a/examples/web/vite-chat-app/src/App.tsx +++ b/examples/web/vite-chat-app/src/App.tsx @@ -1,23 +1,25 @@ import { useState } from "react"; -import { clearCurrentClient } from "./utils/momento-web"; import ChatRoom from "./components/chat-room"; +function determineCognitoUser() { + return import.meta.env.VITE_TOKEN_VENDING_MACHINE_AUTH_TYPE === "cognito" ? "ReadOnly" : "ReadWrite"; +} + export default function Home() { const [topic, setTopic] = useState(""); const [username, setUsername] = useState(""); - const [cognitoUser, setCognitoUser] = useState(import.meta.env.VITE_TOKEN_VENDING_MACHINE_AUTH_TYPE === "cognito" ? "ReadOnly" : "ReadWrite"); + const [cognitoUser, setCognitoUser] = useState(determineCognitoUser()); const [chatRoomSelected, setChatRoomSelected] = useState(false); const [usernameSelected, setUsernameSelected] = useState(false); const [cognitoUserSelected, setCognitoUserSelected] = useState(false); const leaveChatRoom = () => { - clearCurrentClient(); setChatRoomSelected(false); setUsernameSelected(false); setCognitoUserSelected(false); setTopic(""); setUsername(""); - setCognitoUser("ReadOnly"); + setCognitoUser(determineCognitoUser()); }; if (!import.meta.env.VITE_MOMENTO_CACHE_NAME) { @@ -29,12 +31,13 @@ export default function Home() { if (import.meta.env.VITE_TOKEN_VENDING_MACHINE_AUTH_TYPE === "cognito" && !cognitoUserSelected) { return( -
+