MatrixAI / js-db

Key-Value DB for TypeScript and JavaScript Applications
https://polykey.com
Apache License 2.0
5 stars 0 forks source link

Improvements to Transaction Usage Syntax #51

Closed CMCDragonkai closed 2 years ago

CMCDragonkai commented 2 years ago

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;
  };
}
CMCDragonkai commented 2 years ago

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. https://github.com/MatrixAI/Polykey/issues/297

CMCDragonkai commented 2 years ago

Going to close this for now as the context decorators will be implemented in https://github.com/MatrixAI/Polykey/issues/297.