@decaf-ts/core 0.26.4 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +87 -1
  2. package/dist/core.cjs +1 -1
  3. package/dist/core.cjs.map +1 -1
  4. package/dist/core.js +1 -1
  5. package/dist/core.js.map +1 -1
  6. package/lib/cjs/index.cjs +3 -3
  7. package/lib/cjs/persistence/Adapter.cjs +9 -1
  8. package/lib/cjs/persistence/Adapter.cjs.map +1 -1
  9. package/lib/cjs/persistence/ContextLock.cjs +110 -61
  10. package/lib/cjs/persistence/ContextLock.cjs.map +1 -1
  11. package/lib/cjs/persistence/ObserverHandler.cjs +1 -1
  12. package/lib/cjs/persistence/ObserverHandler.cjs.map +1 -1
  13. package/lib/cjs/persistence/constants.cjs +1 -0
  14. package/lib/cjs/persistence/constants.cjs.map +1 -1
  15. package/lib/cjs/persistence/transactions.cjs +57 -24
  16. package/lib/cjs/persistence/transactions.cjs.map +1 -1
  17. package/lib/cjs/ram/RamAdapter.cjs +2 -2
  18. package/lib/cjs/ram/RamAdapter.cjs.map +1 -1
  19. package/lib/esm/index.js +3 -3
  20. package/lib/esm/persistence/Adapter.js +10 -2
  21. package/lib/esm/persistence/Adapter.js.map +1 -1
  22. package/lib/esm/persistence/ContextLock.js +108 -59
  23. package/lib/esm/persistence/ContextLock.js.map +1 -1
  24. package/lib/esm/persistence/ObserverHandler.js +1 -1
  25. package/lib/esm/persistence/ObserverHandler.js.map +1 -1
  26. package/lib/esm/persistence/constants.js +1 -0
  27. package/lib/esm/persistence/constants.js.map +1 -1
  28. package/lib/esm/persistence/transactions.js +55 -22
  29. package/lib/esm/persistence/transactions.js.map +1 -1
  30. package/lib/esm/ram/RamAdapter.js +3 -3
  31. package/lib/esm/ram/RamAdapter.js.map +1 -1
  32. package/lib/types/index.d.cts +3 -3
  33. package/lib/types/index.d.mts +3 -3
  34. package/lib/types/persistence/Adapter.d.cts +10 -2
  35. package/lib/types/persistence/Adapter.d.mts +10 -2
  36. package/lib/types/persistence/ContextLock.d.cts +72 -14
  37. package/lib/types/persistence/ContextLock.d.mts +72 -14
  38. package/lib/types/persistence/transactions.d.cts +20 -3
  39. package/lib/types/persistence/transactions.d.mts +20 -3
  40. package/lib/types/persistence/types.d.cts +9 -0
  41. package/lib/types/persistence/types.d.mts +9 -0
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -39,7 +39,7 @@ Decaf Core provides the foundational building blocks for the Decaf TypeScript ec
39
39
 
40
40
  Documentation [here](https://decaf-ts.github.io/injectable-decorators/), Test results [here](https://decaf-ts.github.io/injectable-decorators/workdocs/reports/html/test-report.html) and Coverage [here](https://decaf-ts.github.io/injectable-decorators/workdocs/reports/coverage/lcov-report/index.html)
41
41
 
42
- Minimal size: 46.9 KB kb gzipped
42
+ Minimal size: 47.1 KB kb gzipped
43
43
 
44
44
 
45
45
  # Core Package — Detailed Description
@@ -320,6 +320,92 @@ class MyCustomAdapter extends Adapter<any, any, any, any> {
320
320
  }
321
321
  ```
322
322
 
323
+ ## Transactions (`@transactional`)
324
+
325
+ `@transactional()` wraps a `Repository` or `Service`/`ModelService` method so every persistence call it makes - including calls to other `@transactional()` methods - runs inside a single transaction boundary. Always import it from `@decaf-ts/core`, **not** from `@decaf-ts/transactional-decorators`: the base package's own `transactional()` factory re-registers its (no-op-for-this-purpose) decorator every time it is used, so importing it anywhere in your process will silently override `@decaf-ts/core`'s implementation.
326
+
327
+ ```typescript
328
+ import { Repository, transactional } from '@decaf-ts/core';
329
+ import { User } from './models';
330
+
331
+ class UserRepository extends Repository<User, any> {
332
+ @transactional()
333
+ async createPair(first: User, second: User, ...args: any[]): Promise<[User, User]> {
334
+ // both creates run inside the same transaction
335
+ const created1 = await this.create(first, ...args);
336
+ const created2 = await this.create(second, ...args);
337
+ return [created1, created2];
338
+ }
339
+
340
+ @transactional()
341
+ async createTriplet(a: User, b: User, c: User, ...args: any[]): Promise<User[]> {
342
+ const pair = await this.createPair(a, b, ...args); // reuses the SAME transaction
343
+ const third = await this.create(c, ...args);
344
+ return [...pair, third];
345
+ }
346
+ }
347
+ ```
348
+
349
+ Key points:
350
+
351
+ * **Always forward the trailing `...args`** between `@transactional()` methods (and into the `create`/`read`/`update`/`delete` calls you make from inside them). That trailing argument carries the `Context` that holds the active lock; drop it and a nested call will start its own, unrelated transaction.
352
+ * **Nesting is free and automatic.** The first `@transactional()` call to run acquires the lock (`lock.begin()`); calls nested inside it detect the lock already on the `Context` and reuse it, only incrementing a depth counter. The transaction only commits (`lock.commit()`) when the outermost call returns successfully, no matter how many levels deep the nesting goes or how many operations each level performs.
353
+ * **One error ends the whole transaction.** If any nested call throws, the transaction rolls back (`lock.rollback(err)`) exactly once and the error propagates - enclosing frames detect the lock is already done and don't roll back a second time.
354
+ * **`Service`/`ModelService` methods follow the exact same contract** - you can mix `@transactional()` Repository and Service methods in the same call tree and they will share one lock.
355
+ * **No configurable isolation level** is provided by the decorator itself; that's left to whatever the underlying adapter's native transaction mechanism supports. The default lock does support capping how many transactions can run concurrently - see below.
356
+
357
+ ### Limiting concurrent transactions (`maxConcurrentTransactions`)
358
+
359
+ The default `ContextLock` (used by any adapter that doesn't override `transactionLock()`, e.g. `RamAdapter`) is gated by the `maxConcurrentTransactions` flag on `AdapterFlags`:
360
+
361
+ * **`-1` (the default)** - no limit; `begin`/`commit`/`rollback` behave as a no-op, exactly like before this flag existed.
362
+ * **`0`** - transactions are disabled outright; every `@transactional()` call immediately throws an `UnsupportedError`.
363
+ * **any positive number `N`** - at most `N` transactions run concurrently on that adapter. A `@transactional()` call beyond the limit queues (via an internal counting semaphore) until one of the `N` in-flight transactions commits or rolls back, then proceeds in FIFO order.
364
+
365
+ Set it like any other flag, typically via `Repository.override(...)`:
366
+
367
+ ```typescript
368
+ const repo = userRepository.override({ maxConcurrentTransactions: 1 });
369
+ // at most one @transactional() call through `repo` runs at a time;
370
+ // a second concurrent call waits until the first commits or rolls back
371
+ await Promise.all([
372
+ repo.someTransactionalMethod(),
373
+ repo.someTransactionalMethod(),
374
+ ]);
375
+ ```
376
+
377
+ The limit is shared by every `ContextLock` created for the same adapter instance (not per-call), so it caps total concurrent transactions against that adapter regardless of which repository or service triggered them.
378
+
379
+ ### How adapters provide native transactions
380
+
381
+ An adapter with real transactional storage (e.g. a SQL database) overrides `transactionLock()` to return a `ContextLock` subclass that wraps its native `BEGIN`/`COMMIT`/`ROLLBACK` equivalent instead of relying on the default semaphore-gated implementation:
382
+
383
+ ```typescript
384
+ import { Adapter, ContextLock } from '@decaf-ts/core';
385
+
386
+ class MyNativeLock extends ContextLock<MyAdapter> {
387
+ override async begin(): Promise<void> {
388
+ // open a dedicated connection/transaction handle on this.adapter
389
+ }
390
+ override async commit(): Promise<void> {
391
+ // commit and release the handle
392
+ }
393
+ override async rollback(): Promise<void> {
394
+ // roll back and release the handle
395
+ }
396
+ }
397
+
398
+ class MyAdapter extends Adapter<any, any, any, any> {
399
+ override transactionLock(...args: any[]): MyNativeLock {
400
+ return new MyNativeLock(this, ...args);
401
+ }
402
+ }
403
+ ```
404
+
405
+ All the bookkeeping (reusing the lock across nested calls, tracking depth, deciding when to actually call `begin`/`commit`/`rollback`) is owned by the `@transactional()` proxy itself - your `ContextLock` subclass only needs to know how to start, end, and undo a transaction once. `@decaf-ts/for-typeorm`'s `TypeORMContextLock` is a complete real-world example of this pattern, backed by Postgres; see its "How to Use" guide for a worked example with real CRUD operations and concurrent transactions.
406
+
407
+ Note that overriding `begin`/`commit`/`rollback` without calling `super.*()` (as `TypeORMContextLock` does) opts out of the `maxConcurrentTransactions` semaphore entirely - native adapters typically have their own, more appropriate way to manage concurrency (connection pooling, native locks, isolation levels), so `for-typeorm`'s documentation explicitly calls out that the flag has no effect there.
408
+
323
409
  ## Services
324
410
 
325
411
  The `ModelService` provides a convenient way to interact with your repositories.