-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: extension module creation (#688)
Signed-off-by: Ariel Gentile <[email protected]>
- Loading branch information
Showing
24 changed files
with
702 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<h1 align="center"><b>Extension module example</b></h1> | ||
|
||
This example shows how can an extension module be written and injected to an Aries Framework Javascript `Agent` instance. Its structure is similar to the one of regular modules, although is not strictly needed to follow it to achieve this goal. | ||
|
||
An extension module could be used for different purposes, such as storing data in an Identity Wallet, supporting custom protocols over Didcomm or implementing new [Aries RFCs](https://github.com/hyperledger/aries-rfcs/tree/main/features) without the need of embed them right into AFJ's Core package. Injected modules can access to other core modules and services and trigger events, so in practice they work much in the same way as if they were included statically. | ||
|
||
## Dummy module | ||
|
||
This example consists of a module that implements a very simple request-response protocol called Dummy. In order to do so and be able to be injected into an AFJ instance, some steps were followed: | ||
|
||
- Define Dummy protocol message classes (inherited from `AgentMessage`) | ||
- Create handlers for those messages (inherited from `Handler`) | ||
- Define records (inherited from `BaseRecord`) and a container-scoped repository (inherited from `Repository`) for state persistance | ||
- Define events (inherited from `BaseEvent`) | ||
- Create a container-scoped service class that manages records and repository, and also trigger events using Agent's `EventEmitter` | ||
- Create a container-scoped module class that registers handlers in Agent's `Dispatcher` and provides a simple API to do requests and responses, with the aid of service classes and Agent's `MessageSender` | ||
|
||
## Usage | ||
|
||
In order to use this module, it must be injected into an AFJ instance. This can be done by resolving DummyModule right after agent is instantiated: | ||
|
||
```ts | ||
import { DummyModule } from './dummy' | ||
|
||
const agent = new Agent(/** agent config... */) | ||
|
||
const dummyModule = agent.injectionContainer.resolve(DummyModule) | ||
|
||
await agent.initialize() | ||
``` | ||
|
||
Then, Dummy module API methods can be called, and events listeners can be created: | ||
|
||
```ts | ||
agent.events.on(DummyEventTypes.StateChanged, async (event: DummyStateChangedEvent) => { | ||
if (event.payload.dummyRecord.state === DummyState.RequestReceived) { | ||
await dummyModule.respond(event.payload.dummyRecord) | ||
} | ||
}) | ||
|
||
const record = await dummyModule.request(connection) | ||
``` | ||
|
||
## Run demo | ||
|
||
This repository includes a demonstration of a requester and a responder controller using this module to exchange Dummy protocol messages. For environment set up, make sure you followed instructions for [NodeJS](/docs/setup-nodejs.md). | ||
|
||
These are the steps for running it: | ||
|
||
Clone the AFJ git repository: | ||
|
||
```sh | ||
git clone https://github.com/hyperledger/aries-framework-javascript.git | ||
``` | ||
|
||
Open two different terminals and go to the extension-module directory: | ||
|
||
```sh | ||
cd aries-framework-javascript/samples/extension-module | ||
``` | ||
|
||
Install the project in one of the terminals: | ||
|
||
```sh | ||
yarn install | ||
``` | ||
|
||
In that terminal run the responder: | ||
|
||
```sh | ||
yarn responder | ||
``` | ||
|
||
Wait for it to finish the startup process (i.e. logger showing 'Responder listening to port ...') and run requester in another terminal: | ||
|
||
```sh | ||
yarn requester | ||
``` | ||
|
||
If everything goes right, requester will connect to responder and, as soon as connection protocol is finished, it will send a Dummy request. Responder will answer with a Dummy response and requester will happily exit. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import type { DummyRecord } from './repository/DummyRecord' | ||
import type { ConnectionRecord } from '@aries-framework/core' | ||
|
||
import { ConnectionService, Dispatcher, MessageSender } from '@aries-framework/core' | ||
import { Lifecycle, scoped } from 'tsyringe' | ||
|
||
import { DummyRequestHandler, DummyResponseHandler } from './handlers' | ||
import { DummyState } from './repository' | ||
import { DummyService } from './services' | ||
|
||
@scoped(Lifecycle.ContainerScoped) | ||
export class DummyModule { | ||
private messageSender: MessageSender | ||
private dummyService: DummyService | ||
private connectionService: ConnectionService | ||
|
||
public constructor( | ||
dispatcher: Dispatcher, | ||
messageSender: MessageSender, | ||
dummyService: DummyService, | ||
connectionService: ConnectionService | ||
) { | ||
this.messageSender = messageSender | ||
this.dummyService = dummyService | ||
this.connectionService = connectionService | ||
this.registerHandlers(dispatcher) | ||
} | ||
|
||
/** | ||
* Send a Dummy Request | ||
* | ||
* @param connection record of the target responder (must be active) | ||
* @returns created Dummy Record | ||
*/ | ||
public async request(connection: ConnectionRecord) { | ||
const { record, message: payload } = await this.dummyService.createRequest(connection) | ||
|
||
await this.messageSender.sendMessage({ connection, payload }) | ||
|
||
await this.dummyService.updateState(record, DummyState.RequestSent) | ||
|
||
return record | ||
} | ||
|
||
/** | ||
* Respond a Dummy Request | ||
* | ||
* @param record Dummy record | ||
* @returns Updated dummy record | ||
*/ | ||
public async respond(record: DummyRecord) { | ||
if (!record.connectionId) { | ||
throw new Error('Connection not found!') | ||
} | ||
|
||
const connection = await this.connectionService.getById(record.connectionId) | ||
|
||
const payload = await this.dummyService.createResponse(record) | ||
|
||
await this.messageSender.sendMessage({ connection, payload }) | ||
|
||
await this.dummyService.updateState(record, DummyState.ResponseSent) | ||
|
||
return record | ||
} | ||
|
||
/** | ||
* Retrieve all dummy records | ||
* | ||
* @returns List containing all records | ||
*/ | ||
public getAll(): Promise<DummyRecord[]> { | ||
return this.dummyService.getAll() | ||
} | ||
|
||
private registerHandlers(dispatcher: Dispatcher) { | ||
dispatcher.registerHandler(new DummyRequestHandler(this.dummyService)) | ||
dispatcher.registerHandler(new DummyResponseHandler(this.dummyService)) | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
samples/extension-module/dummy/handlers/DummyRequestHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { DummyService } from '../services' | ||
import type { Handler, HandlerInboundMessage } from '@aries-framework/core' | ||
|
||
import { DummyRequestMessage } from '../messages' | ||
|
||
export class DummyRequestHandler implements Handler { | ||
public supportedMessages = [DummyRequestMessage] | ||
private dummyService: DummyService | ||
|
||
public constructor(dummyService: DummyService) { | ||
this.dummyService = dummyService | ||
} | ||
|
||
public async handle(inboundMessage: HandlerInboundMessage<DummyRequestHandler>) { | ||
inboundMessage.assertReadyConnection() | ||
|
||
await this.dummyService.processRequest(inboundMessage) | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
samples/extension-module/dummy/handlers/DummyResponseHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { DummyService } from '../services' | ||
import type { Handler, HandlerInboundMessage } from '@aries-framework/core' | ||
|
||
import { DummyResponseMessage } from '../messages' | ||
|
||
export class DummyResponseHandler implements Handler { | ||
public supportedMessages = [DummyResponseMessage] | ||
private dummyService: DummyService | ||
|
||
public constructor(dummyService: DummyService) { | ||
this.dummyService = dummyService | ||
} | ||
|
||
public async handle(inboundMessage: HandlerInboundMessage<DummyResponseHandler>) { | ||
inboundMessage.assertReadyConnection() | ||
|
||
await this.dummyService.processResponse(inboundMessage) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './DummyRequestHandler' | ||
export * from './DummyResponseHandler' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export * from './DummyModule' | ||
export * from './handlers' | ||
export * from './messages' | ||
export * from './services' | ||
export * from './repository' |
20 changes: 20 additions & 0 deletions
20
samples/extension-module/dummy/messages/DummyRequestMessage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { AgentMessage } from '@aries-framework/core' | ||
import { Equals } from 'class-validator' | ||
|
||
export interface DummyRequestMessageOptions { | ||
id?: string | ||
} | ||
|
||
export class DummyRequestMessage extends AgentMessage { | ||
public constructor(options: DummyRequestMessageOptions) { | ||
super() | ||
|
||
if (options) { | ||
this.id = options.id ?? this.generateId() | ||
} | ||
} | ||
|
||
@Equals(DummyRequestMessage.type) | ||
public readonly type = DummyRequestMessage.type | ||
public static readonly type = 'https://didcomm.org/dummy/1.0/request' | ||
} |
24 changes: 24 additions & 0 deletions
24
samples/extension-module/dummy/messages/DummyResponseMessage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { AgentMessage } from '@aries-framework/core' | ||
import { Equals } from 'class-validator' | ||
|
||
export interface DummyResponseMessageOptions { | ||
id?: string | ||
threadId: string | ||
} | ||
|
||
export class DummyResponseMessage extends AgentMessage { | ||
public constructor(options: DummyResponseMessageOptions) { | ||
super() | ||
|
||
if (options) { | ||
this.id = options.id ?? this.generateId() | ||
this.setThread({ | ||
threadId: options.threadId, | ||
}) | ||
} | ||
} | ||
|
||
@Equals(DummyResponseMessage.type) | ||
public readonly type = DummyResponseMessage.type | ||
public static readonly type = 'https://2060.io/didcomm/dummy/response' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './DummyRequestMessage' | ||
export * from './DummyResponseMessage' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import type { DummyState } from './DummyState' | ||
|
||
import { BaseRecord } from '@aries-framework/core' | ||
import { v4 as uuid } from 'uuid' | ||
|
||
export interface DummyStorageProps { | ||
id?: string | ||
createdAt?: Date | ||
connectionId?: string | ||
threadId: string | ||
state: DummyState | ||
} | ||
|
||
export class DummyRecord extends BaseRecord implements DummyStorageProps { | ||
public connectionId?: string | ||
public threadId!: string | ||
public state!: DummyState | ||
|
||
public static readonly type = 'DummyRecord' | ||
public readonly type = DummyRecord.type | ||
|
||
public constructor(props: DummyStorageProps) { | ||
super() | ||
if (props) { | ||
this.id = props.id ?? uuid() | ||
this.createdAt = props.createdAt ?? new Date() | ||
this.state = props.state | ||
this.connectionId = props.connectionId | ||
this.threadId = props.threadId | ||
} | ||
} | ||
|
||
public getTags() { | ||
return { | ||
...this._tags, | ||
threadId: this.threadId, | ||
connectionId: this.connectionId, | ||
state: this.state, | ||
} | ||
} | ||
|
||
public assertState(expectedStates: DummyState | DummyState[]) { | ||
if (!Array.isArray(expectedStates)) { | ||
expectedStates = [expectedStates] | ||
} | ||
|
||
if (!expectedStates.includes(this.state)) { | ||
throw new Error(`Dummy record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.`) | ||
} | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
samples/extension-module/dummy/repository/DummyRepository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Repository, StorageService, InjectionSymbols } from '@aries-framework/core' | ||
import { inject, scoped, Lifecycle } from 'tsyringe' | ||
|
||
import { DummyRecord } from './DummyRecord' | ||
|
||
@scoped(Lifecycle.ContainerScoped) | ||
export class DummyRepository extends Repository<DummyRecord> { | ||
public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService<DummyRecord>) { | ||
super(DummyRecord, storageService) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export enum DummyState { | ||
Init = 'init', | ||
RequestSent = 'request-sent', | ||
RequestReceived = 'request-received', | ||
ResponseSent = 'response-sent', | ||
ResponseReceived = 'response-received', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './DummyRecord' | ||
export * from './DummyRepository' | ||
export * from './DummyState' |
Oops, something went wrong.