Improvements to Transaction Usage Syntax
CMCDragonkai opened this issue · comments
Is your feature request related to a problem? Please describe.
The usage of transactions could benefit from syntax helpers.
Right now without having a monadic expression, methods have to thread the DBTransaction
.
class X {
public async f(tran?: DBTransaction) {
if (tran == null) {
return this.db.withTransactionF(
(tran) => this.f.apply(this, [...arguments, tran])
);
}
// use tran
}
}
Describe the solution you'd like
Instead something like this could be possible:
class X {
@transaction(this.db)
public async g(tran: DBTransaction) {
// use tran
}
}
This is currently not possible until decorators are able to change the method signatures. That is, currently TS decorators are not allowed to change method signatures so then tran
here is actually required by the caller.
At the same time, the TypedDescriptor
should be used to allow a union of different methods. Any method that takes the tran: DBTransaction
at the very end of the function is allowed. The usage of withTransactionF
and withTransactionG
is the only ones allowed, as one must do it inside an async function or an async generator function.
At the same time, this syntax enhancement only works for functions with a very specific signature.
Describe alternatives you've considered
The main alternative is a monadic API. Monads have a bind
function where M a -> (a -> M b) -> M b
.
Here the Transaction
is the monadic context, that functions are bound to.
If we were in Haskell we could do:
tran >>= (\s -> return s) >> put "key" "value" >> get "key"
What is the monad actually wrapping? It's not the database... I suppose it is the state. Then the state can be interrogated there. But the get
and put
would be transactional methods. It could really wrap anything.
To allow do syntax to be possible, one must make our ;
the equivalent of the bind
operator.
class X {
public async f(): DBTransaction<void> {
put();
get();
return;
}
}
Then put
and get
represent transactional operators, that can only composed with an existing monad.
This isn't idiomatic JS atm, so it's not really possible.
The other way is to use this
and then use class decorator mixins that enable the context of the function to be augmented with operators that make it transactional. But that adds even more magic.
Once method signatures can be changed, then it would be worthwhile for application placed decorators to do this. It could also work if instead of parameters, we had keyword/named parameters. Which would make a @transaction
decorator alot more robust.
Another way is to allow users to define their decorator functions easily, so that way they can address different kind of function signatures. Like have the transaction be the first parameter, last parameter or anywhere in between.
Without method signature changes, you would need to use tran!
inside the function to ensure that TS understands that's it is in fact always defined.
Additional context
function transaction(db: { a: string }) {
return (
target: any,
key: string,
descriptor: TypedPropertyDescriptor<(tran: string) => any>
) => {
const f = descriptor.value;
if (typeof f !== 'function') {
throw new TypeError(`${key} is not a function`);
}
descriptor.value = function (tran: string) {
return this.db.withTransactionF(
(tran) => f.apply(this, [...arguments, tran])
);
};
return descriptor;
};
}
We are making this work inside PK. We have a way of overloading the decorator signatures, and with parameter decorators, we can specify where the context should be located. However since our context interface also contains other properties like signal and timer, right now it's still a thing inside PK, and cannot be factored out to independent library. Could be useful if we had an open interface for the context and allowed one to share the context map. MatrixAI/Polykey#297
Going to close this for now as the context decorators will be implemented in MatrixAI/Polykey#297.