|
1 | 1 | # Cardano JS SDK | projection
|
2 | 2 |
|
3 |
| -Chain Sync event projection utilities. |
| 3 | +This package is a library of generic projection types and utilities. |
4 | 4 |
|
5 |
| -## Summary |
| 5 | +> [!TIP] |
| 6 | +> [Guide to Projections and Read Models in Event-Driven Architecture](https://event-driven.io/en/projections_and_read_models_in_event_driven_architecture/) |
6 | 7 |
|
7 |
| -Projection is based on [RxJS](https://rxjs.dev/), where source observable of Chain Sync events is processed with various [operators](./src/operators/). |
| 8 | +If you're interested in projecting into PostgreSQL database, see [projection-typeorm](../projection-typeorm) package. |
8 | 9 |
|
9 |
| -There are no restrictions what an operator can do - you can utilize the full power of RxJS which makes it very flexible. |
| 10 | +If you're interested in projector application as run by Lace, see [setup](../cardano-services/src/Projection) and [README](../cardano-services/README.md) in `cardano-services` package. |
10 | 11 |
|
11 |
| -All operators implemented in this package are extending the source event object with extra properties, e.g. |
| 12 | +## Prerequisites |
12 | 13 |
|
13 |
| -```ts |
14 |
| -Bootstrap.fromCardanoNode({ buffer, cardanoNode, logger }).pipe( |
15 |
| - Mappers.withStakeKeys(), |
16 |
| - tap(({ stakeKeys }) => console.log('stakeKeys', stakeKeys)), |
17 |
| - Mappers.withStakePools(), |
18 |
| - tap(({ stakePools }) => console.log('stakePools', stakePools)) |
| 14 | +In order to understand how `cardano-js-sdk` projections work, you'll first need basic understanding of: |
| 15 | + |
| 16 | +- [Observables](https://github.com/tc39/proposal-observable) (we're using [RxJS](https://rxjs.dev/) implementation) |
| 17 | +- Chain Sync mini protocol (well explained in [Ogmios docs](https://ogmios.dev/mini-protocols/local-chain-sync/)) |
| 18 | + |
| 19 | +## Types |
| 20 | + |
| 21 | +### ProjectionEvent\<ExtraProps> |
| 22 | + |
| 23 | +All projections start by obtaining a source/producer<sup>1</sup>, which is an `Observable<ProjectionEvent<{}>>`. These events are very similar to Chain Sync events, but there are some important differences: |
| 24 | + |
| 25 | +1. Block format is compatible with types from `@cardano-sdk/core` package. |
| 26 | +2. Events include some additional properties: `{eraSumaries, genesisParameters, epochNo, crossEpochBoundary}`. |
| 27 | +3. `RollBackward` events include block data (instead of just specifying the rollback point), and are emitted once for **each** rolled back block. |
| 28 | + |
| 29 | +#### ExtraProps (Generic Parameter) |
| 30 | + |
| 31 | +Source observable can be piped through a series of RxJS operators, which may add some properties to the event. In a nutshell, `ProjectionEvent<T> = ProjectionEvent<{}> & T`. For more detail see [Mappers](#mappers). |
| 32 | + |
| 33 | +### StabilityWindowBuffer |
| 34 | + |
| 35 | +Since `ProjectionEvent{eventType=RollBackward}` includes block data, we must store some number (at least `k`, which is the "security parameter") of latest blocks so that if a fork/rollback happens, we can call `StabilityWindowBuffer.getBlock(hash)` in order to build a valid `ProjectionEvent`. |
| 36 | + |
| 37 | +### ObservableCardanoNode |
| 38 | + |
| 39 | +This is our interface to Chain Sync and Local State Query protocols of Cardano node. The only implementation available in `cardano-js-sdk` is `OgmiosObservableCardanoNode`. |
| 40 | + |
| 41 | +## Utilities |
| 42 | + |
| 43 | +### Bootstrap.fromCardanoNode |
| 44 | + |
| 45 | +Creates a projection source (`Observable<ProjectionEvent>`) from: |
| 46 | + |
| 47 | +- [ObservableCardanoNode](#observablecardanonode) |
| 48 | +- [StabilityWindowBuffer](#stabilitywindowbuffer) |
| 49 | +- `Observable<TipOrOrigin>` that emits block header of last processed event (local tip). This is used to find intersection point. |
| 50 | + |
| 51 | +```mermaid |
| 52 | +--- |
| 53 | +title: Bootstrap Algorithm |
| 54 | +--- |
| 55 | +flowchart LR |
| 56 | + subscribe[Subscribe] --> find-intersect{Find node\nintersection\nwith local tip} |
| 57 | + find-intersect --> |Found| start[Start ChainSync\nfrom intersection] |
| 58 | + start --> get-event{Get event\nfrom node,\ncheck type} |
| 59 | + get-event --> |RollBackward| get-block[Get block from StabilityWindowBuffer] |
| 60 | + get-event --> |RollForward| hydrate |
| 61 | + start --> hydrate[Hydrate event\nwith additional\npropertes] |
| 62 | + hydrate --> emit[Emit\nProjectionEvent] |
| 63 | + find-intersect --> |Not Found| get-block[Get block from\nStabilityWindowBuffer] |
| 64 | + get-block --> rollback[Create\nRollBackward\nevent] |
| 65 | + rollback --> hydrate |
| 66 | + rollback --> wait[Wait for\nlocal tip change] |
| 67 | + wait --> is-rollback-point{Is rollback point?} |
| 68 | + is-rollback-point --> |No| get-block |
| 69 | + is-rollback-point --> |Yes| started-sync{Already started\nChain Sync?} |
| 70 | + started-sync --> |Yes| get-event |
| 71 | + started-sync --> |No| find-intersect |
| 72 | +``` |
| 73 | + |
| 74 | +### Mappers |
| 75 | + |
| 76 | +Projections are typically |
| 77 | + |
| 78 | +1. Interpreting the block (e.g. extracting some relevant properties from all transactions). `Mappers` is a namespace that contains a collection of RxJS operators that process the ProjectionEvent (block) and emit a new event object that has some additional properties. |
| 79 | +2. Doing something with it (e.g. store into the database). |
| 80 | + |
| 81 | +Here's an example: |
| 82 | + |
| 83 | +```typescript |
| 84 | +// source$: Observable<ProjectionEvent<{}>> |
| 85 | +source$.pipe( |
| 86 | + // policyId is a parameter that should be provided |
| 87 | + // by handle issuer, e.g. ADA Handle |
| 88 | + Mappers.withHandles({ policyId }), |
| 89 | + // type WithHandles = { handles: HandleOwnership; } |
| 90 | + // Event type can also be inferred (no need for explicit type declaration) |
| 91 | + tap((event: ProjectionEvent<WithHandles>) => { |
| 92 | + console.log('Handles found by withHandles operator', event.handles); |
| 93 | + }) |
19 | 94 | );
|
20 | 95 | ```
|
| 96 | + |
| 97 | +### Other Utils |
| 98 | + |
| 99 | +#### InMemoryStabilityWindowBuffer |
| 100 | + |
| 101 | +If your application doesn't need persistence, you can use in-memory implementation of [StabilityWindowBuffer](#stabilitywindowbuffer), which can also be used as local tip tracker for Bootstrap (provides `Observable<TipOrOrigin>`). |
| 102 | + |
| 103 | +--- |
| 104 | + |
| 105 | +_<sup>1</sup>Currently our projection source observable is not a pure 'Producer', as events have a `requestNext` method that is used to control the source. This is subject to change in the future._ |
0 commit comments