From 00b0ce76a126c78999a1bebc77029fff9d4ea24a Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:39:51 +0300 Subject: [PATCH 01/13] Add interrupter factory --- workers/loc.api/di/factories/index.js | 4 ++- .../di/factories/interrupter-factory.js | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 workers/loc.api/di/factories/interrupter-factory.js diff --git a/workers/loc.api/di/factories/index.js b/workers/loc.api/di/factories/index.js index e0c1d5945..d0cde3b95 100644 --- a/workers/loc.api/di/factories/index.js +++ b/workers/loc.api/di/factories/index.js @@ -7,6 +7,7 @@ const syncFactory = require('./sync-factory') const processMessageManagerFactory = require('./process-message-manager-factory') const syncUserStepDataFactory = require('./sync-user-step-data-factory') const wsEventEmitterFactory = require('./ws-event-emitter-factory') +const interrupterFactory = require('./interrupter-factory') module.exports = { migrationsFactory, @@ -15,5 +16,6 @@ module.exports = { syncFactory, processMessageManagerFactory, syncUserStepDataFactory, - wsEventEmitterFactory + wsEventEmitterFactory, + interrupterFactory } diff --git a/workers/loc.api/di/factories/interrupter-factory.js b/workers/loc.api/di/factories/interrupter-factory.js new file mode 100644 index 000000000..866644474 --- /dev/null +++ b/workers/loc.api/di/factories/interrupter-factory.js @@ -0,0 +1,29 @@ +'use strict' + +const { + AuthError +} = require('bfx-report/workers/loc.api/errors') + +const TYPES = require('../types') + +module.exports = (ctx) => { + const authenticator = ctx.container.get(TYPES.Authenticator) + + return (params) => { + const { user, name } = params ?? {} + + if (!user) { + throw new AuthError() + } + + const interrupter = ctx.container.get( + TYPES.Interrupter + ).setName(name) + + authenticator.setInterrupterToUserSession( + user, interrupter + ) + + return interrupter + } +} From 0142633d280d02d147951d37f62c81fe6e192506 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:40:21 +0300 Subject: [PATCH 02/13] Add InterrupterFactory service type --- workers/loc.api/di/types.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workers/loc.api/di/types.js b/workers/loc.api/di/types.js index 0f1286839..316ac85fa 100644 --- a/workers/loc.api/di/types.js +++ b/workers/loc.api/di/types.js @@ -70,5 +70,6 @@ module.exports = { SyncUserStepDataFactory: Symbol.for('SyncUserStepDataFactory'), HTTPRequest: Symbol.for('HTTPRequest'), SummaryByAsset: Symbol.for('SummaryByAsset'), - TransactionTaxReport: Symbol.for('TransactionTaxReport') + TransactionTaxReport: Symbol.for('TransactionTaxReport'), + InterrupterFactory: Symbol.for('InterrupterFactory') } From 2b198aede592fe6ba2fd9933ba212046c8c0a134 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:41:44 +0300 Subject: [PATCH 03/13] Add InterrupterFactory service into di --- workers/loc.api/di/app.deps.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/workers/loc.api/di/app.deps.js b/workers/loc.api/di/app.deps.js index 84a4f57bf..ccf75527f 100644 --- a/workers/loc.api/di/app.deps.js +++ b/workers/loc.api/di/app.deps.js @@ -112,7 +112,8 @@ const { syncFactory, processMessageManagerFactory, syncUserStepDataFactory, - wsEventEmitterFactory + wsEventEmitterFactory, + interrupterFactory } = require('./factories') const Crypto = require('../sync/crypto') const Authenticator = require('../sync/authenticator') @@ -343,6 +344,8 @@ module.exports = ({ bind(TYPES.SyncInterrupter) .to(SyncInterrupter) .inSingletonScope() + bind(TYPES.InterrupterFactory) + .toFactory(interrupterFactory) bind(TYPES.Movements) .to(Movements) bind(TYPES.WinLossVSAccountBalance) From 8d20289f2e47792796a4fb74728e03a3a2496f59 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:43:57 +0300 Subject: [PATCH 04/13] Add ability to process interrupters on sign-out --- workers/loc.api/sync/authenticator/index.js | 102 +++++++++++++++++++- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/workers/loc.api/sync/authenticator/index.js b/workers/loc.api/sync/authenticator/index.js index 79f98a06d..31ededdaf 100644 --- a/workers/loc.api/sync/authenticator/index.js +++ b/workers/loc.api/sync/authenticator/index.js @@ -10,6 +10,9 @@ const { isENetError, isAuthError } = require('bfx-report/workers/loc.api/helpers') +const Interrupter = require( + 'bfx-report/workers/loc.api/interrupter' +) const { serializeVal } = require('../dao/helpers') const { @@ -501,6 +504,7 @@ class Authenticator { throw new AuthError() } + await this._processInterrupters(user) this.removeUserSession({ email, isSubAccount, @@ -750,7 +754,7 @@ class Authenticator { ) { const session = this.getUserSessionByToken( token, - isReturnedPassword + { isReturnedPassword } ) const { authToken, apiKey, apiSecret } = session ?? {} @@ -1105,6 +1109,40 @@ class Authenticator { return true } + setInterrupterToUserSession (user, interrupter) { + const userSession = this.getUserSessionByEmail( + user, + { shouldOrigObjRefBeReturned: true } + ) + + if (!userSession) { + throw new AuthError() + } + + userSession.interrupters = userSession.interrupters instanceof Set + ? userSession.interrupters + : new Set() + + if (!(interrupter instanceof Interrupter)) { + return + } + + interrupter.onceInterrupted(() => { + userSession.interrupters.delete(interrupter) + }) + + userSession.interrupters.add(interrupter) + } + + async interruptOperations (args) { + await this._processInterrupters( + args?.auth, + args?.params?.names + ) + + return true + } + setUserSession (user) { const { token, @@ -1135,17 +1173,35 @@ class Authenticator { this.userTokenMapByEmail.set(tokenKey, token) } - getUserSessionByToken (token, isReturnedPassword) { + getUserSessionByToken (token, opts) { + const { + isReturnedPassword, + shouldOrigObjRefBeReturned + } = opts ?? {} + const session = this.userSessions.get(token) + if (shouldOrigObjRefBeReturned) { + return session + } + return pickSessionProps(session, isReturnedPassword) } - getUserSessionByEmail (user, isReturnedPassword) { + getUserSessionByEmail (user, opts) { + const { + isReturnedPassword, + shouldOrigObjRefBeReturned + } = opts ?? {} + const tokenKey = this._getTokenKeyByEmailField(user) const token = this.userTokenMapByEmail.get(tokenKey) const session = this.userSessions.get(token) + if (shouldOrigObjRefBeReturned) { + return session + } + return pickSessionProps(session, isReturnedPassword) } @@ -1518,6 +1574,46 @@ class Authenticator { ) ) } + + async _processInterrupters (user, names) { + const _names = Array.isArray(names) + ? names + : [names] + const filteredName = _names.filter((name) => ( + name && + typeof name === 'string' + )) + const userSession = this.getUserSessionByEmail( + user, + { shouldOrigObjRefBeReturned: true } + ) + + if ( + !(userSession?.interrupters instanceof Set) || + userSession.interrupters.size === 0 + ) { + return [] + } + + const promises = [] + const interrupters = [...userSession.interrupters] + .filter((int) => ( + filteredName.length === 0 || + filteredName.some((name) => name === int.name) + )) + + for (const interrupter of interrupters) { + userSession.interrupters.delete(interrupter) + + if (!(interrupter instanceof Interrupter)) { + continue + } + + promises.push(interrupter.interrupt()) + } + + return await Promise.all(promises) + } } decorateInjectable(Authenticator, depsTypes) From 1045b30c18434f99bc9e9e66931ad3525f3d0060 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:45:05 +0300 Subject: [PATCH 05/13] Add interrupter into lookUpTrades helper --- .../helpers/look-up-trades.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/workers/loc.api/sync/transaction.tax.report/helpers/look-up-trades.js b/workers/loc.api/sync/transaction.tax.report/helpers/look-up-trades.js index a1b4b002d..64cf0ba69 100644 --- a/workers/loc.api/sync/transaction.tax.report/helpers/look-up-trades.js +++ b/workers/loc.api/sync/transaction.tax.report/helpers/look-up-trades.js @@ -20,7 +20,8 @@ module.exports = async (trades, opts) => { isBackIterativeSaleLookUp = false, isBackIterativeBuyLookUp = false, isBuyTradesWithUnrealizedProfitRequired = false, - isNotGainOrLossRequired = false + isNotGainOrLossRequired = false, + interrupter } = opts ?? {} const saleTradesWithRealizedProfit = [] @@ -42,6 +43,13 @@ module.exports = async (trades, opts) => { : trades for (const [i, trade] of tradeIterator.entries()) { + if (interrupter?.hasInterrupted()) { + return { + saleTradesWithRealizedProfit, + buyTradesWithUnrealizedProfit + } + } + const currentLoopUnlockMts = Date.now() /* @@ -162,6 +170,12 @@ module.exports = async (trades, opts) => { ) for (let j = startPoint; checkPoint(j); j = shiftPoint(j)) { + if (interrupter?.hasInterrupted()) { + return { + saleTradesWithRealizedProfit, + buyTradesWithUnrealizedProfit + } + } if (trade.isSaleTrxHistFilled) { break } From c588f8ac4a879735cc569fe12a967aba9aa74e13 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:46:50 +0300 Subject: [PATCH 06/13] Add ability to interrupt trx tax report --- .../sync/transaction.tax.report/index.js | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/workers/loc.api/sync/transaction.tax.report/index.js b/workers/loc.api/sync/transaction.tax.report/index.js index 4d5b510d5..1bf8a5c4b 100644 --- a/workers/loc.api/sync/transaction.tax.report/index.js +++ b/workers/loc.api/sync/transaction.tax.report/index.js @@ -1,5 +1,9 @@ 'use strict' +const INTERRUPTER_NAMES = require( + 'bfx-report/workers/loc.api/interrupter/interrupter.names' +) + const { pushLargeArr } = require('../../helpers/utils') const { getBackIterable } = require('../helpers') const { PubTradeFindForTrxTaxError } = require('../../errors') @@ -25,7 +29,8 @@ const depsTypes = (TYPES) => [ TYPES.RService, TYPES.GetDataFromApi, TYPES.WSEventEmitterFactory, - TYPES.Logger + TYPES.Logger, + TYPES.InterrupterFactory ] class TransactionTaxReport { constructor ( @@ -38,7 +43,8 @@ class TransactionTaxReport { rService, getDataFromApi, wsEventEmitterFactory, - logger + logger, + interrupterFactory ) { this.dao = dao this.authenticator = authenticator @@ -50,6 +56,7 @@ class TransactionTaxReport { this.getDataFromApi = getDataFromApi this.wsEventEmitterFactory = wsEventEmitterFactory this.logger = logger + this.interrupterFactory = interrupterFactory this.tradesModel = this.syncSchema.getModelsMap() .get(this.ALLOWED_COLLS.TRADES) @@ -79,6 +86,10 @@ class TransactionTaxReport { const strategy = params.strategy ?? TRX_TAX_STRATEGIES.LIFO const user = await this.authenticator .verifyRequestUser({ auth }) + const interrupter = this.interrupterFactory({ + user, + name: INTERRUPTER_NAMES.TRX_TAX_REPORT_INTERRUPTER + }) const isFIFO = strategy === TRX_TAX_STRATEGIES.FIFO const isLIFO = strategy === TRX_TAX_STRATEGIES.LIFO @@ -96,6 +107,8 @@ class TransactionTaxReport { !Array.isArray(trxsForCurrPeriod) || trxsForCurrPeriod.length === 0 ) { + interrupter.emitInterrupted() + return [] } @@ -118,7 +131,8 @@ class TransactionTaxReport { isBackIterativeSaleLookUp, isBackIterativeBuyLookUp, isBuyTradesWithUnrealizedProfitRequired: true, - isNotGainOrLossRequired: true + isNotGainOrLossRequired: true, + interrupter } ) @@ -131,16 +145,23 @@ class TransactionTaxReport { !Number.isFinite(trx?.lastSymbPriceUsd) )) ) - await this.#convertCurrencies(trxsForConvToUsd) + await this.#convertCurrencies(trxsForConvToUsd, { interrupter }) const { saleTradesWithRealizedProfit } = await lookUpTrades( trxsForCurrPeriod, { isBackIterativeSaleLookUp, - isBackIterativeBuyLookUp + isBackIterativeBuyLookUp, + interrupter } ) + interrupter.emitInterrupted() + + if (interrupter.hasInterrupted()) { + return [] + } + return saleTradesWithRealizedProfit } @@ -202,9 +223,14 @@ class TransactionTaxReport { } async #convertCurrencies (trxs, opts) { + const { interrupter } = opts const trxMapByCcy = getTrxMapByCcy(trxs) for (const [symbol, trxPriceCalculators] of trxMapByCcy.entries()) { + if (interrupter.hasInterrupted()) { + return + } + const trxPriceCalculatorIterator = getBackIterable(trxPriceCalculators) let pubTrades = [] @@ -212,6 +238,10 @@ class TransactionTaxReport { let pubTradeEnd = pubTrades[pubTrades.length - 1]?.mts for (const trxPriceCalculator of trxPriceCalculatorIterator) { + if (interrupter.hasInterrupted()) { + return + } + const { trx } = trxPriceCalculator if ( From 5ff92a64cc065da85c5b96cc6f399fb2ba4b2516 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:49:21 +0300 Subject: [PATCH 07/13] Set name into sync interrupter --- workers/loc.api/sync/sync.interrupter/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/workers/loc.api/sync/sync.interrupter/index.js b/workers/loc.api/sync/sync.interrupter/index.js index 5f28d4b52..865c0dd60 100644 --- a/workers/loc.api/sync/sync.interrupter/index.js +++ b/workers/loc.api/sync/sync.interrupter/index.js @@ -3,6 +3,9 @@ const Interrupter = require( 'bfx-report/workers/loc.api/interrupter' ) +const INTERRUPTER_NAMES = require( + 'bfx-report/workers/loc.api/interrupter/interrupter.names' +) const SYNC_PROGRESS_STATES = require('../progress/sync.progress.states') @@ -20,6 +23,7 @@ class SyncInterrupter extends Interrupter { this.INITIAL_PROGRESS = 'SYNCHRONIZATION_HAS_NOT_BEEN_STARTED_TO_INTERRUPT' this.INTERRUPTED_PROGRESS = SYNC_PROGRESS_STATES.INTERRUPTED_PROGRESS + this.setName(INTERRUPTER_NAMES.SYNC_INTERRUPTER) this._init() } From 3f3a8a9dcd4abba4b2290b1cd4418e888c76e64f Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:51:12 +0300 Subject: [PATCH 08/13] Add interruptOperations endpoint --- workers/loc.api/service.report.framework.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/workers/loc.api/service.report.framework.js b/workers/loc.api/service.report.framework.js index 2e0ea6bf7..c6631ddb9 100644 --- a/workers/loc.api/service.report.framework.js +++ b/workers/loc.api/service.report.framework.js @@ -256,7 +256,9 @@ class FrameworkReportService extends ReportService { getPlatformStatus (space, args, cb) { return this._responder(async () => { - const rest = this._getREST({}) + const rest = this._getREST({}, { + interrupter: args?.interrupter + }) const res = await rest.status() const isMaintenance = !Array.isArray(res) || !res[0] @@ -463,6 +465,14 @@ class FrameworkReportService extends ReportService { }, 'stopSyncNow', args, cb) } + interruptOperations (space, args = {}, cb) { + return this._privResponder(() => { + checkParams(args, 'paramsSchemaForInterruptOperations', ['names']) + + return this._authenticator.interruptOperations(args) + }, 'interruptOperations', args, cb) + } + getPublicTradesConf (space, args = {}, cb) { return this._privResponder(() => { return this._publicCollsConfAccessors @@ -720,7 +730,8 @@ class FrameworkReportService extends ReportService { (args) => this._getDataFromApi({ getData: (space, args) => super.getActivePositions(space, args), args, - callerName: 'ACTIVE_POSITIONS_GETTER' + callerName: 'ACTIVE_POSITIONS_GETTER', + shouldNotInterrupt: true }), args, { @@ -760,7 +771,8 @@ class FrameworkReportService extends ReportService { return super.getPositionsAudit(space, args) }, args, - callerName: 'POSITIONS_AUDIT_GETTER' + callerName: 'POSITIONS_AUDIT_GETTER', + shouldNotInterrupt: true }), args, { From 868a594aaf9b5b3f5f7dbad61f46a39850d158e8 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:51:43 +0300 Subject: [PATCH 09/13] Add params schema for interruptOperations endpoint --- workers/loc.api/helpers/schema.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/workers/loc.api/helpers/schema.js b/workers/loc.api/helpers/schema.js index 0059fd710..da5b45021 100644 --- a/workers/loc.api/helpers/schema.js +++ b/workers/loc.api/helpers/schema.js @@ -49,6 +49,22 @@ const paramsSchemaForUpdateSubAccount = { } } +const paramsSchemaForInterruptOperations = { + type: 'object', + properties: { + names: { + type: 'array', + minItems: 1, + items: { + type: 'string', + enum: [ + 'TRX_TAX_REPORT_INTERRUPTER' + ] + } + } + } +} + const paramsSchemaForCandlesApi = { ...cloneDeep(baseParamsSchemaForCandlesApi), properties: { @@ -482,6 +498,7 @@ module.exports = { paramsSchemaForEditCandlesConf, paramsSchemaForCreateSubAccount, paramsSchemaForUpdateSubAccount, + paramsSchemaForInterruptOperations, paramsSchemaForBalanceHistoryApi, paramsSchemaForWinLossApi, paramsSchemaForWinLossVSAccountBalanceApi, From f7331f6db4e3369f28d1cd72bb8081cce1c062a1 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:53:25 +0300 Subject: [PATCH 10/13] Implement interruption ability in BfxApiRouter service --- workers/loc.api/bfx.api.router/index.js | 29 +++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/workers/loc.api/bfx.api.router/index.js b/workers/loc.api/bfx.api.router/index.js index 5a011cd60..cabbba25f 100644 --- a/workers/loc.api/bfx.api.router/index.js +++ b/workers/loc.api/bfx.api.router/index.js @@ -3,6 +3,9 @@ const BaseBfxApiRouter = require( 'bfx-report/workers/loc.api/bfx.api.router' ) +const Interrupter = require( + 'bfx-report/workers/loc.api/interrupter' +) const RateLimitChecker = require('./rate.limit.checker') @@ -62,7 +65,7 @@ class BfxApiRouter extends BaseBfxApiRouter { /** * @override */ - route (methodName, method) { + route (methodName, method, interrupter) { if ( !methodName || methodName.startsWith('_') @@ -83,9 +86,27 @@ class BfxApiRouter extends BaseBfxApiRouter { if (rateLimitChecker.check()) { // Cool down delay - return new Promise((resolve) => ( - setTimeout(resolve, this._coolDownDelayMs)) - ).then(() => { + return new Promise((resolve) => { + const onceInterruptHandler = () => { + clearTimeout(timeout) + resolve() + } + const setTimeoutHandler = () => { + if (interrupter instanceof Interrupter) { + interrupter.offInterrupt(onceInterruptHandler) + } + + resolve() + } + + const timeout = setTimeout(setTimeoutHandler, this._coolDownDelayMs) + + if (!(interrupter instanceof Interrupter)) { + return + } + + interrupter.onceInterrupt(onceInterruptHandler) + }).then(() => { rateLimitChecker.add() return method() From 37ab0fa01638dea00a7ae056b554d7514c73a2b2 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:54:15 +0300 Subject: [PATCH 11/13] Turn off interrupter for full snapshot report csv writer --- .../csv-writer/full-snapshot-report-csv-writer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workers/loc.api/generate-report-file/csv-writer/full-snapshot-report-csv-writer.js b/workers/loc.api/generate-report-file/csv-writer/full-snapshot-report-csv-writer.js index 044d8f04a..9bc1fc10a 100644 --- a/workers/loc.api/generate-report-file/csv-writer/full-snapshot-report-csv-writer.js +++ b/workers/loc.api/generate-report-file/csv-writer/full-snapshot-report-csv-writer.js @@ -47,7 +47,8 @@ module.exports = ( const res = await getDataFromApi({ getData: rService[name].bind(rService), args, - callerName: 'REPORT_FILE_WRITER' + callerName: 'REPORT_FILE_WRITER', + shouldNotInterrupt: true }) const { From a44b9a8a3515e2b5ca6bacf132f57b99dcab7833 Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:54:41 +0300 Subject: [PATCH 12/13] Turn off interrupter for full tax report csv writer --- .../csv-writer/full-tax-report-csv-writer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workers/loc.api/generate-report-file/csv-writer/full-tax-report-csv-writer.js b/workers/loc.api/generate-report-file/csv-writer/full-tax-report-csv-writer.js index c120c4547..15578c0e5 100644 --- a/workers/loc.api/generate-report-file/csv-writer/full-tax-report-csv-writer.js +++ b/workers/loc.api/generate-report-file/csv-writer/full-tax-report-csv-writer.js @@ -48,7 +48,8 @@ module.exports = ( const res = await getDataFromApi({ getData: rService[name].bind(rService), args, - callerName: 'REPORT_FILE_WRITER' + callerName: 'REPORT_FILE_WRITER', + shouldNotInterrupt: true }) const { timestamps, From 34ca33141e5b814aba498ef1918a06cf5836a14a Mon Sep 17 00:00:00 2001 From: Vladimir Voronkov Date: Mon, 24 Jun 2024 08:56:03 +0300 Subject: [PATCH 13/13] Turn off interrupter for sub.account.api.data service --- workers/loc.api/sync/sub.account.api.data/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workers/loc.api/sync/sub.account.api.data/index.js b/workers/loc.api/sync/sub.account.api.data/index.js index 1570ac7a7..839ff2634 100644 --- a/workers/loc.api/sync/sub.account.api.data/index.js +++ b/workers/loc.api/sync/sub.account.api.data/index.js @@ -189,7 +189,8 @@ class SubAccountApiData { const res = await this.getDataFromApi({ getData: (space, args) => method(args), args, - callerName: 'SUB_ACCOUNT_API_DATA' + callerName: 'SUB_ACCOUNT_API_DATA', + shouldNotInterrupt: true }) resArr.push(res)