Skip to content

Commit

Permalink
feat: extension module creation (#688)
Browse files Browse the repository at this point in the history
Signed-off-by: Ariel Gentile <[email protected]>
  • Loading branch information
genaris authored Apr 4, 2022
1 parent abec3a2 commit 2b6441a
Show file tree
Hide file tree
Showing 24 changed files with 702 additions and 5 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"license": "Apache-2.0",
"workspaces": [
"packages/*",
"demo"
"demo",
"samples/*"
],
"repository": {
"url": "https://github.com/hyperledger/aries-framework-javascript",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@stablelib/sha256": "^1.0.1",
"@types/indy-sdk": "^1.16.16",
"@types/node-fetch": "^2.5.10",
"@types/ws": "^7.4.4",
"@types/ws": "^7.4.6",
"abort-controller": "^3.0.0",
"bn.js": "^5.2.0",
"borc": "^3.0.0",
Expand All @@ -42,7 +42,7 @@
"object-inspect": "^1.10.3",
"query-string": "^7.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.1.0",
"rxjs": "^7.2.0",
"tsyringe": "^4.5.0",
"uuid": "^8.3.2",
"varint": "^6.0.0",
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import 'reflect-metadata'

export { Agent } from './agent/Agent'
export { BaseEvent } from './agent/Events'
export { EventEmitter } from './agent/EventEmitter'
export { Handler, HandlerInboundMessage } from './agent/Handler'
export { InboundMessageContext } from './agent/models/InboundMessageContext'
export { AgentConfig } from './agent/AgentConfig'
export { AgentMessage } from './agent/AgentMessage'
export { Dispatcher } from './agent/Dispatcher'
Expand All @@ -10,7 +14,10 @@ export type { AgentDependencies } from './agent/AgentDependencies'
export type { InitConfig, OutboundPackage, EncryptedMessage } from './types'
export { DidCommMimeType } from './types'
export type { FileSystem } from './storage/FileSystem'
export { BaseRecord } from './storage/BaseRecord'
export { InMemoryMessageRepository } from './storage/InMemoryMessageRepository'
export { Repository } from './storage/Repository'
export { StorageService } from './storage/StorageService'
export { getDirFromFilePath } from './utils/path'
export { InjectionSymbols } from './constants'
export type { Wallet } from './wallet/Wallet'
Expand Down
80 changes: 80 additions & 0 deletions samples/extension-module/README.md
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.
80 changes: 80 additions & 0 deletions samples/extension-module/dummy/DummyModule.ts
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 samples/extension-module/dummy/handlers/DummyRequestHandler.ts
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 samples/extension-module/dummy/handlers/DummyResponseHandler.ts
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)
}
}
2 changes: 2 additions & 0 deletions samples/extension-module/dummy/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './DummyRequestHandler'
export * from './DummyResponseHandler'
5 changes: 5 additions & 0 deletions samples/extension-module/dummy/index.ts
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 samples/extension-module/dummy/messages/DummyRequestMessage.ts
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 samples/extension-module/dummy/messages/DummyResponseMessage.ts
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'
}
2 changes: 2 additions & 0 deletions samples/extension-module/dummy/messages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './DummyRequestMessage'
export * from './DummyResponseMessage'
51 changes: 51 additions & 0 deletions samples/extension-module/dummy/repository/DummyRecord.ts
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 samples/extension-module/dummy/repository/DummyRepository.ts
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)
}
}
7 changes: 7 additions & 0 deletions samples/extension-module/dummy/repository/DummyState.ts
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',
}
3 changes: 3 additions & 0 deletions samples/extension-module/dummy/repository/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './DummyRecord'
export * from './DummyRepository'
export * from './DummyState'
Loading

0 comments on commit 2b6441a

Please sign in to comment.