Skip to content

Commit

Permalink
refactor: memoize sync/asyncParamResolver methods
Browse files Browse the repository at this point in the history
  • Loading branch information
simonfan committed Mar 17, 2021
1 parent 76b70ca commit d5b01f1
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 208 deletions.
3 changes: 1 addition & 2 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
- $and | Add 'strict' mode option (src/expressions/logical.ts)
- paramResolver | Study paramResolver memoization (src/interpreter/asyncParamResolver.ts)

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@
"typescript": "^4.0.2"
},
"dependencies": {
"@orioro/typing": "^4.3.0",
"lodash": "^4.17.20"
"@orioro/typing": "^4.4.0",
"lodash": "^4.17.20",
"memoizee": "^0.4.15"
},
"config": {
"commitizen": {
Expand Down
10 changes: 6 additions & 4 deletions src/interpreter/asyncInterpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ import { InterpreterSpecSingle, InterpreterFunction } from '../types'

import { asyncParamResolver } from './asyncParamResolver'

import { castTypeSpec } from '@orioro/typing'

export const asyncInterpreter = (
spec: InterpreterSpecSingle
): InterpreterFunction => {
const [
fn,
paramResolvers,
{ defaultParam = paramResolvers.length - 1 } = {},
paramTypeSpecs,
{ defaultParam = paramTypeSpecs.length - 1 } = {},
] = spec

//
// Bring all evaluation logic that is possible
// to outside the returned interperter wrapper function
// in order to minimize expression evaluation performance
//
const asyncParamResolvers = paramResolvers.map((resolver) =>
asyncParamResolver(resolver)
const asyncParamResolvers = paramTypeSpecs.map((typeSpec) =>
asyncParamResolver(castTypeSpec(typeSpec))
)

return (context, ...args) =>
Expand Down
187 changes: 95 additions & 92 deletions src/interpreter/asyncParamResolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import memoize from 'memoizee/weak'

import {
castTypeSpec,
ANY_TYPE,
SINGLE_TYPE,
ONE_OF_TYPES,
Expand All @@ -8,11 +9,12 @@ import {
INDEFINITE_OBJECT_OF_TYPE,
TUPLE_TYPE,
OBJECT_TYPE,
NonShorthandTypeSpec,
} from '@orioro/typing'

import { validateType, isType } from '../typing'

import { TypeSpec, ParamResolver } from '../types'
import { ParamResolver } from '../types'

import { evaluate, evaluateTypedAsync } from '../evaluate'
import { promiseResolveObject } from '../util/promiseResolveObject'
Expand All @@ -21,110 +23,111 @@ import { _pseudoSymbol } from '../util/misc'

const _NOT_RESOLVED = _pseudoSymbol()

const _asyncParamResolver = (typeSpec: TypeSpec): ParamResolver => {
const expectedType = castTypeSpec(typeSpec)

if (expectedType === null) {
throw new TypeError(`Invalid typeSpec: ${JSON.stringify(typeSpec)}`)
}
const _asyncParamResolver = memoize(
(typeSpec: NonShorthandTypeSpec): ParamResolver => {
if (typeSpec.skipEvaluation) {
return (context, value) => Promise.resolve(value)
}

if (expectedType.skipEvaluation) {
return (context, value) => Promise.resolve(value)
}
switch (typeSpec.specType) {
case ANY_TYPE:
case SINGLE_TYPE:
case ENUM_TYPE:
return (context, value) => Promise.resolve(evaluate(context, value))
case ONE_OF_TYPES: {
// This resolver is quite costly: it attempts to resolve
// against each of the listed possible types
const candidateResolverPairs: [
NonShorthandTypeSpec,
ParamResolver
][] = typeSpec.types.map((type) => [type, _asyncParamResolver(type)])

return (context, value) => {
return candidateResolverPairs
.reduce((accPromise, [candidateType, candidateResolver]) => {
return accPromise.then((acc) => {
if (acc !== _NOT_RESOLVED) {
return acc
} else {
return candidateResolver(context, value).then((result) => {
return isType(candidateType, result)
? result
: _NOT_RESOLVED
})
}
})
}, Promise.resolve(_NOT_RESOLVED))
.then((result) => (result === _NOT_RESOLVED ? value : result))
}
}
case TUPLE_TYPE: {
const itemParamResolvers = typeSpec.items.map((itemResolver) =>
_asyncParamResolver(itemResolver)
)

switch (expectedType.specType) {
case ANY_TYPE:
case SINGLE_TYPE:
case ENUM_TYPE:
return (context, value) => Promise.resolve(evaluate(context, value))
case ONE_OF_TYPES: {
// This resolver is quite costly: it attempts to resolve
// against each of the listed possible types
const candidateResolverPairs: [
TypeSpec,
ParamResolver
][] = expectedType.types.map((type) => [type, _asyncParamResolver(type)])

return (context, value) => {
return candidateResolverPairs
.reduce((accPromise, [candidateType, candidateResolver]) => {
return accPromise.then((acc) => {
if (acc !== _NOT_RESOLVED) {
return acc
} else {
return candidateResolver(context, value).then((result) => {
return isType(candidateType, result) ? result : _NOT_RESOLVED
})
}
})
}, Promise.resolve(_NOT_RESOLVED))
.then((result) => (result === _NOT_RESOLVED ? value : result))
return (context, value) => {
return evaluateTypedAsync('array', context, value).then((array) => {
return Promise.all(
array.map((item, index) => {
return itemParamResolvers[index](context, item)
})
)
})
}
}
}
case TUPLE_TYPE: {
const itemParamResolvers = expectedType.items.map((itemResolver) =>
_asyncParamResolver(itemResolver)
)

return (context, value) => {
return evaluateTypedAsync('array', context, value).then((array) => {
return Promise.all(
array.map((item, index) => {
return itemParamResolvers[index](context, item)
})
)
})
case INDEFINITE_ARRAY_OF_TYPE: {
const itemParamResolver = _asyncParamResolver(typeSpec.itemType)

return (context, value) => {
return evaluateTypedAsync('array', context, value).then((array) => {
return Promise.all(
array.map((item) => itemParamResolver(context, item))
)
})
}
}
}
case INDEFINITE_ARRAY_OF_TYPE: {
const itemParamResolver = _asyncParamResolver(expectedType.itemType)
case OBJECT_TYPE: {
const propertyParamResolvers = Object.keys(typeSpec.properties).reduce(
(acc, key) => ({
...acc,
[key]: _asyncParamResolver(typeSpec.properties[key]),
}),
{}
)

return (context, value) => {
return evaluateTypedAsync('array', context, value).then((array) => {
return Promise.all(
array.map((item) => itemParamResolver(context, item))
return (context, value) =>
evaluateTypedAsync(
'object',
context,
value
).then((unresolvedObject) =>
promiseResolveObject(
unresolvedObject,
(propertyValue, propertyKey) =>
propertyParamResolvers[propertyKey](context, propertyValue)
)
)
})
}
}
case OBJECT_TYPE: {
const propertyParamResolvers = Object.keys(
expectedType.properties
).reduce(
(acc, key) => ({
...acc,
[key]: _asyncParamResolver(expectedType.properties[key]),
}),
{}
)

return (context, value) =>
evaluateTypedAsync('object', context, value).then((unresolvedObject) =>
promiseResolveObject(unresolvedObject, (propertyValue, propertyKey) =>
propertyParamResolvers[propertyKey](context, propertyValue)
case INDEFINITE_OBJECT_OF_TYPE: {
const propertyParamResolver = _asyncParamResolver(typeSpec.propertyType)

return (context, value) =>
evaluateTypedAsync('object', context, value).then((object) =>
promiseResolveObject(object, (propertyValue) =>
propertyParamResolver(context, propertyValue)
)
)
)
}
case INDEFINITE_OBJECT_OF_TYPE: {
const propertyParamResolver = _asyncParamResolver(
expectedType.propertyType
)

return (context, value) =>
evaluateTypedAsync('object', context, value).then((object) =>
promiseResolveObject(object, (propertyValue) =>
propertyParamResolver(context, propertyValue)
)
)
}
}
}
}
)

/**
* @todo paramResolver Study paramResolver memoization
* @function asyncParamResolver
*/
export const asyncParamResolver = (typeSpec: TypeSpec): ParamResolver => {
export const asyncParamResolver = (
typeSpec: NonShorthandTypeSpec
): ParamResolver => {
// `_asyncParamResolver` (private) contains the logic for the resolution itself
// but does not run any kind of validation
// validation is executed at `asyncParamResolver` (public)
Expand Down
11 changes: 3 additions & 8 deletions src/interpreter/paramResolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { testCases, asyncResult } from '@orioro/jest-util'

import { ALL_EXPRESSIONS } from '../'
import {
castTypeSpec,
anyType,
tupleType,
indefiniteArrayOfType,
Expand All @@ -15,6 +16,8 @@ import { asyncParamResolver } from './asyncParamResolver'
const interpreters = interpreterList(ALL_EXPRESSIONS)

const _resolverTestCases = (cases, paramSpec) => {
paramSpec = castTypeSpec(paramSpec)

const syncResolver = syncParamResolver(paramSpec)
const asyncResolver = asyncParamResolver(paramSpec)

Expand Down Expand Up @@ -56,14 +59,6 @@ const _resolverTestCases = (cases, paramSpec) => {
)
}

test('invalid type', () => {
expect(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
syncParamResolver(10)
}).toThrow('Invalid typeSpec')
})

describe('anyType()', () => {
_resolverTestCases(
[
Expand Down
4 changes: 3 additions & 1 deletion src/interpreter/syncInterpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { InterpreterSpecSingle, InterpreterFunction } from '../types'

import { syncParamResolver } from './syncParamResolver'

import { castTypeSpec } from '@orioro/typing'

/**
* @function syncInterpreter
* @returns {Interpreter}
Expand All @@ -21,7 +23,7 @@ export const syncInterpreter = (
// in order to minimize expression evaluation performance
//
const syncParamResolvers = paramTypeSpecs.map((typeSpec) =>
syncParamResolver(typeSpec)
syncParamResolver(castTypeSpec(typeSpec))
)

return (context, ...args) =>
Expand Down
Loading

0 comments on commit d5b01f1

Please sign in to comment.