Skip to content

Commit

Permalink
Add AsyncLocalStorage driver (#39)
Browse files Browse the repository at this point in the history
* Remove `bindEmitter` call

After doing research on what `bindEmitter` does, I could not see the
benefits of using it. At the time the hooks run, the transaction is already
over. There seem to be no reason to preserve store context for userland
use.

All current tests pass. There is no mention of preserved context in the
README, therefore this should be treated as internal change.

This change is motivated by the fact AsyncLocalStorage does not support
`bindEmitter` out of the box and without understanding the end goal it
is not feasible to properly reimplement it.

If at any point in the future a viable use-case is provided, the bindEmitter
can easily be restored.

* Add StorageLayer interface and implementations

This change introduces a concept of StorageLayer, an interface which
can be implemented using various storage providers. Currently the
implementations include AsyncLocalStorage and cls-hooked.

* Abstract direct cls-hooked use with StorageLayer

This commit abstracts the use of cls-hooked to StorageLayer.

It also updates the StorageLayer provider to always use cls-hooked
implementation, as AsyncLocalStorage implementation
does not pass tests yet.

* Implement AsyncLocalStorage

The change implements AsyncLocalStorage driver, which passes
all existing tests. As the actual logic relies on cls-hooked behavior
a Store class is introduces to emulate it within AsyncLocalStorage.

* Rename `implementation` to `driver`.

Separating name refactor out of previous commit for readability

* Allow users to specify StorageDriver

* Add env to determine StorageDriver in test

* Default to CLS_HOOKED when no option specified

* Uninstall cross-env

* Typo in getBestSupportedDriverConstructor

* Remove TODO from error message

* Update comment

* fix: remove unsupported node: protocol

* cicd: add matrix strategy to ci pipeline

* docs: describe StorageDriver enum in the readme

---------

Co-authored-by: aliheym <aliheym.dev@gmail.com>
  • Loading branch information
Aareksio and Aliheym authored Oct 19, 2023
1 parent dbebc75 commit 14daba3
Show file tree
Hide file tree
Showing 17 changed files with 2,892 additions and 1,116 deletions.
33 changes: 19 additions & 14 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
name: main
on: [push]

jobs:
main:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
version: ['12.x', '18.x', '20.x']
storageDriver: ['ASYNC_LOCAL_STORAGE', 'CLS_HOOKED']
steps:
- uses: actions/checkout@v2

- name: Check If Tag
id: check-tag
run: |-
if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo ::set-output name=match::true
fi
- uses: actions/checkout@v4

- uses: actions/setup-node@v1
- uses: actions/setup-node@v3
with:
node-version: '12.x'
node-version: ${{ matrix.version }}
registry-url: 'https://registry.npmjs.org'

- name: Install
run: npm install

- name: Test
run: npm test
env:
TEST_STORAGE_DRIVER: ${{ matrix.storageDriver }}

publish:
if: startsWith(github.event.ref, 'refs/tags/v')
needs: tests
runs-on: ubuntu-latest
steps:
- name: Install
run: npm install

- name: Build
run: npm run build

- name: Publish
if: steps.check-tag.outputs.match == 'true'
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}


40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## It's a fork of [typeorm-transactional-cls-hooked](https://github.com/odavid/typeorm-transactional-cls-hooked) for new versions of TypeORM.


A `Transactional` Method Decorator for [typeorm](http://typeorm.io/) that uses [cls-hooked](https://www.npmjs.com/package/cls-hooked) to handle and propagate transactions between different repositories and service methods.
A `Transactional` Method Decorator for [typeorm](http://typeorm.io/) that uses [ALS](https://nodejs.org/api/async_context.html#class-asynclocalstorage) or [cls-hooked](https://www.npmjs.com/package/cls-hooked) to handle and propagate transactions between different repositories and service methods.

See [Changelog](#CHANGELOG.md)

Expand All @@ -23,9 +23,10 @@ See [Changelog](#CHANGELOG.md)
- [API](#api)
- [Library Options](#library-options)
- [Transaction Options](#transaction-options)
- [initializeTransactionalContext(options): void](#initializetransactionalcontext-void)
- [Storage Driver](#storage-driver)
- [initializeTransactionalContext(options): void](#initializetransactionalcontextoptions-void)
- [addTransactionalDataSource(input): DataSource](#addtransactionaldatasourceinput-datasource)
- [runInTransaction(fn: Callback, options?: Options): Promise<...>](#runintransactionfn-callback-options-options-promise)
- [runInTransaction(fn: Callback, options?: Options): Promise\<...\>](#runintransactionfn-callback-options-options-promise)
- [wrapInTransaction(fn: Callback, options?: Options): WrappedFunction](#wrapintransactionfn-callback-options-options-wrappedfunction)
- [runOnTransactionCommit(cb: Callback): void](#runontransactioncommitcb-callback-void)
- [runOnTransactionRollback(cb: Callback): void](#runontransactionrollbackcb-callback-void)
Expand Down Expand Up @@ -53,12 +54,12 @@ yarn add typeorm reflect-metadata
## Initialization

In order to use it, you will first need to initialize the cls-hooked namespace before your application is started
In order to use it, you will first need to initialize the transactional context before your application is started

```typescript
import { initializeTransactionalContext } from 'typeorm-transactional';

initializeTransactionalContext() // Initialize cls-hooked
initializeTransactionalContext()
...
app = express()
...
Expand All @@ -77,7 +78,7 @@ To be able to use TypeORM entities in transactions, you must first add a DataSou

```typescript
import { DataSource } from 'typeorm';
import { initializeTransactionalContext, addTransactionalDataSource } from 'typeorm-transactional';
import { initializeTransactionalContext, addTransactionalDataSource, StorageDriver } from 'typeorm-transactional';
...
const dataSource = new DataSource({
type: 'postgres',
Expand All @@ -88,7 +89,7 @@ const dataSource = new DataSource({
});
...

initializeTransactionalContext();
initializeTransactionalContext({ storageDriver: StorageDriver.ASYNC_LOCAL_STORAGE });
addTransactionalDataSource(dataSource);

...
Expand Down Expand Up @@ -298,10 +299,11 @@ Repositories, services, etc. can be mocked as usual.

```typescript
{
storageDriver?: StorageDriver,
maxHookHandlers?: number
}
```

- `storageDriver` - Determines which [underlying mechanism](#storage-driver) (like Async Local Storage or cls-hooked) the library should use for handling and propagating transactions. By default, it's `StorageDriver.CLS_HOOKED`.
- `maxHookHandlers` - Controls how many hooks (`commit`, `rollback`, `complete`) can be used simultaneously. If you exceed the number of hooks of same type, you get a warning. This is a useful to find possible memory leaks. You can set this options to `0` or `Infinity` to indicate an unlimited number of listeners. By default, it's `10`.

### Transaction Options
Expand All @@ -314,13 +316,31 @@ Repositories, services, etc. can be mocked as usual.
}
```

- `connectionName`- DataSource` name to use for this transactional context ([the data sources](#data-sources))
- `connectionName`- DataSource name to use for this transactional context ([the data sources](#data-sources))
- `isolationLevel`- isolation level for transactional context ([isolation levels](#isolation-levels) )
- `propagation`- propagation behaviors for nest transactional contexts ([propagation behaviors](#transaction-propagation))

### Storage Driver

Option that determines which underlying mechanism the library should use for handling and propagating transactions.

The possible variants:

- `AUTO` - Automatically selects the appropriate storage mechanism based on the Node.js version, using `AsyncLocalStorage` for Node.js versions 16 and above, and defaulting to `cls-hooked` for earlier versions.
- `CLS_HOOKED` - Utilizes the `cls-hooked` package to provide context storage, supporting both legacy Node.js versions with AsyncWrap for versions below 8.2.1, and using `async_hooks` for later versions.
- `ASYNC_LOCAL_STORAGE` - Uses the built-in `AsyncLocalStorage` feature, available from Node.js version 16 onwards,

> ⚠️ **WARNING:** Currently, we use `CLS_HOOKED` by default for backward compatibility. However, in the next major release, this default will be switched to `AUTO`.
```typescript
import { StorageDriver } from 'typeorm-transactional'

initializeTransactionalContext({ storageDriver: StorageDriver.AUTO });
```

### initializeTransactionalContext(options): void

Initialize `cls-hooked` namespace.
Initialize transactional context.

```typescript
initializeTransactionalContext(options?: TypeormTransactionalOptions);
Expand Down
Loading

0 comments on commit 14daba3

Please sign in to comment.