diff --git a/package-lock.json b/package-lock.json index d60ac6f..cf6d2d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.4.0", "dependencies": { "@tbd54566975/dwn-sdk-js": "0.4.0", - "@tbd54566975/dwn-sql-store": "0.6.0", + "@tbd54566975/dwn-sql-store": "0.6.1", "better-sqlite3": "^8.5.0", "body-parser": "^1.20.2", "bytes": "3.1.2", @@ -671,12 +671,12 @@ } }, "node_modules/@tbd54566975/dwn-sql-store": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sql-store/-/dwn-sql-store-0.6.0.tgz", - "integrity": "sha512-9o9W2A/gXsmj+n+H5debmOrQelybS9g2sPxFcB46zvT8Zpe9OAKUB9j8s7o1XEeUflyqAW1gf6+q3KKO/UhPHQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sql-store/-/dwn-sql-store-0.6.1.tgz", + "integrity": "sha512-4vxOPnP85mbCOMQ1SZZ4s5gJqIZSPdxUfWViBKnEYdXeNgdw1Cfmt6Hu0Y+E493iIoSrE0JYX7V8mS9vzJIVvw==", "dependencies": { "@ipld/dag-cbor": "9.0.5", - "@tbd54566975/dwn-sdk-js": "0.4.0", + "@tbd54566975/dwn-sdk-js": "0.4.1", "kysely": "0.26.3", "multiformats": "12.0.1", "readable-stream": "4.4.2" @@ -698,6 +698,103 @@ "npm": ">=7.0.0" } }, + "node_modules/@tbd54566975/dwn-sql-store/node_modules/@noble/ciphers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tbd54566975/dwn-sql-store/node_modules/@tbd54566975/dwn-sdk-js": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.4.1.tgz", + "integrity": "sha512-AkB0pl5LunxTwBRN/q+jgSRRRkT/6TDqr6SSMLTocXnSqptdTvSymDmGJ3/8d75YNvRsSgAuiVeKA9NoTJG3DQ==", + "dependencies": { + "@ipld/dag-cbor": "9.0.3", + "@js-temporal/polyfill": "0.4.4", + "@noble/ciphers": "0.5.3", + "@noble/ed25519": "2.0.0", + "@noble/secp256k1": "2.0.0", + "@web5/dids": "1.1.0", + "abstract-level": "1.0.3", + "ajv": "8.12.0", + "blockstore-core": "4.2.0", + "cross-fetch": "4.0.0", + "eciesjs": "0.4.5", + "interface-blockstore": "5.2.3", + "interface-store": "5.1.2", + "ipfs-unixfs-exporter": "13.1.5", + "ipfs-unixfs-importer": "15.1.5", + "level": "8.0.0", + "lodash": "4.17.21", + "lru-cache": "9.1.2", + "ms": "2.1.3", + "multiformats": "11.0.2", + "randombytes": "2.1.0", + "readable-stream": "4.5.2", + "ulidx": "2.1.0", + "uuid": "8.3.2", + "varint": "6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@tbd54566975/dwn-sql-store/node_modules/@tbd54566975/dwn-sdk-js/node_modules/@ipld/dag-cbor": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.0.3.tgz", + "integrity": "sha512-A2UFccS0+sARK9xwXiVZIaWbLbPxLGP3UZOjBeOMWfDY04SXi8h1+t4rHBzOlKYF/yWNm3RbFLyclWO7hZcy4g==", + "dependencies": { + "cborg": "^2.0.1", + "multiformats": "^12.0.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tbd54566975/dwn-sql-store/node_modules/@tbd54566975/dwn-sdk-js/node_modules/@ipld/dag-cbor/node_modules/multiformats": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-12.1.3.tgz", + "integrity": "sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw==", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tbd54566975/dwn-sql-store/node_modules/@tbd54566975/dwn-sdk-js/node_modules/cborg": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-2.0.5.tgz", + "integrity": "sha512-xVW1rSIw1ZXbkwl2XhJ7o/jAv0vnVoQv/QlfQxV8a7V5PlA4UU/AcIiXqmpyybwNWy/GPQU1m/aBVNIWr7/T0w==", + "bin": { + "cborg": "cli.js" + } + }, + "node_modules/@tbd54566975/dwn-sql-store/node_modules/@tbd54566975/dwn-sdk-js/node_modules/multiformats": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-11.0.2.tgz", + "integrity": "sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg==", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tbd54566975/dwn-sql-store/node_modules/@tbd54566975/dwn-sdk-js/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/@tbd54566975/dwn-sql-store/node_modules/cborg": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.1.tgz", @@ -715,6 +812,14 @@ "npm": ">=7.0.0" } }, + "node_modules/@tbd54566975/dwn-sql-store/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", diff --git a/package.json b/package.json index db0ba46..a8fc520 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@web5/dwn-server", "type": "module", - "version": "0.4.0", + "version": "0.4.1", "files": [ "dist", "src" @@ -27,7 +27,7 @@ }, "dependencies": { "@tbd54566975/dwn-sdk-js": "0.4.0", - "@tbd54566975/dwn-sql-store": "0.6.0", + "@tbd54566975/dwn-sql-store": "0.6.1", "better-sqlite3": "^8.5.0", "body-parser": "^1.20.2", "bytes": "3.1.2", diff --git a/src/web5-connect/sql-ttl-cache.ts b/src/web5-connect/sql-ttl-cache.ts index 4a0bef1..697b3cb 100644 --- a/src/web5-connect/sql-ttl-cache.ts +++ b/src/web5-connect/sql-ttl-cache.ts @@ -8,11 +8,13 @@ export class SqlTtlCache { private static readonly cacheTableName = 'cacheEntries'; private static readonly cleanupIntervalInSeconds = 60; + private sqlDialect: Dialect; private db: Kysely; private cleanupTimer: NodeJS.Timeout; private constructor(sqlDialect: Dialect) { this.db = new Kysely({ dialect: sqlDialect }); + this.sqlDialect = sqlDialect; } /** @@ -27,14 +29,26 @@ export class SqlTtlCache { } private async initialize(): Promise { - await this.db.schema - .createTable(SqlTtlCache.cacheTableName) - .ifNotExists() - // 512 chars to accommodate potentially large `state` in Web5 Connect flow - .addColumn('key', 'varchar(512)', (column) => column.primaryKey()) - .addColumn('value', 'text', (column) => column.notNull()) - .addColumn('expiry', 'integer', (column) => column.notNull()) - .execute(); + + // create table if it doesn't exist + const tableExists = await this.sqlDialect.hasTable(this.db, SqlTtlCache.cacheTableName); + if (!tableExists) { + await this.db.schema + .createTable(SqlTtlCache.cacheTableName) + .ifNotExists() // kept to show supported by all dialects in contrast to ifNotExists() below, though not needed due to tableExists check above + // 512 chars to accommodate potentially large `state` in Web5 Connect flow + .addColumn('key', 'varchar(512)', (column) => column.primaryKey()) + .addColumn('value', 'text', (column) => column.notNull()) + .addColumn('expiry', 'integer', (column) => column.notNull()) + .execute(); + + await this.db.schema + .createIndex('index_expiry') + // .ifNotExists() // intentionally kept commented out code to show that it is not supported by all dialects (ie. MySQL) + .on(SqlTtlCache.cacheTableName) + .column('expiry') + .execute(); + } // Start the cleanup timer this.startCleanupTimer(); @@ -102,7 +116,7 @@ export class SqlTtlCache { } /** - * Periodically clean up expired cache entries. + * Cleans up expired cache entries. */ public async cleanUpExpiredEntries(): Promise { await this.db diff --git a/src/web5-connect/web5-connect-server.ts b/src/web5-connect/web5-connect-server.ts index df158d0..dc1ee40 100644 --- a/src/web5-connect/web5-connect-server.ts +++ b/src/web5-connect/web5-connect-server.ts @@ -83,10 +83,15 @@ export class Web5ConnectServer { * Returns the Web5 Connect Request object. The request ID can only be used once. */ public async getWeb5ConnectRequest(requestId: string): Promise { - const request = this.cache.get(`request:${requestId}`); + const request = await this.cache.get(`request:${requestId}`); - // Delete the Request Object from the data store now that it has been retrieved. - this.cache.delete(`request:${requestId}`); + // Delete the Request Object from cache once it has been retrieved. + // IMPORTANT: only delete if object is fetched, otherwise there could be a race condition + // where the object is not fetched in this call but become available immediately right, + // we would end up deleting it before a successful fetch. + if (request !== undefined) { + this.cache.delete(`request:${requestId}`); + } return request; } @@ -102,10 +107,15 @@ export class Web5ConnectServer { * Gets the Web5 Connect Response object. The `state` string can only be used once. */ public async getWeb5ConnectResponse(state: string): Promise { - const response = this. cache.get(`response:${state}`); - - // Delete the Response object from the data store now that it has been retrieved. - this.cache.delete(`response:${state}`); + const response = await this.cache.get(`response:${state}`); + + // Delete the Response object from the cache once it has been retrieved. + // IMPORTANT: only delete if object is fetched, otherwise there could be a race condition + // where the object is not fetched in this call but become available immediately right, + // we would end up deleting it before a successful fetch. + if (response !== undefined) { + this.cache.delete(`response:${state}`); + } return response; }