├── .gitignore
├── CHANGELOG.md
├── README.md
├── package-lock.json
├── package.json
├── src
├── index.ts
└── lib
│ ├── actionCreator.ts
│ ├── middleware.test.ts
│ └── middleware.ts
├── tsconfig.json
├── tsconfig.module.json
└── tslint.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | .nyc_output
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 |
6 | ## [1.1.1](https://github.com/holochain/hc-redux-middleware/compare/v1.1.0...v1.1.1) (2019-04-09)
7 |
8 |
9 |
10 |
11 | ## [1.1.1](https://github.com/holochain/hc-redux-middleware/compare/v1.0.9...v1.1.1) (2019-04-09)
12 |
13 |
14 |
15 |
16 | ## [1.0.9](https://github.com/holochain/hc-redux-middleware/compare/v1.0.8...v1.0.9) (2019-01-16)
17 |
18 |
19 |
20 |
21 | ## [1.0.8](https://github.com/holochain/hc-redux-middleware/compare/v1.0.7...v1.0.8) (2018-12-18)
22 |
23 |
24 |
25 |
26 | ## [1.0.7](https://github.com/holochain/hc-redux-middleware/compare/v1.0.6...v1.0.7) (2018-12-11)
27 |
28 |
29 |
30 |
31 | ## [1.0.6](https://github.com/holochain/hc-redux-middleware/compare/v1.0.5...v1.0.6) (2018-12-11)
32 |
33 |
34 |
35 |
36 | ## [1.0.5](https://github.com/holochain/hc-redux-middleware/compare/v1.0.4...v1.0.5) (2018-12-09)
37 |
38 |
39 |
40 |
41 | ## [1.0.4](https://github.com/holochain/hc-redux-middleware/compare/v1.0.3...v1.0.4) (2018-12-09)
42 |
43 |
44 |
45 |
46 | ## [1.0.3](https://github.com/holochain/hc-redux-middleware/compare/v1.0.2...v1.0.3) (2018-12-09)
47 |
48 |
49 |
50 |
51 | ## [1.0.2](https://github.com/holochain/hc-redux-middleware/compare/v1.0.1...v1.0.2) (2018-12-09)
52 |
53 |
54 |
55 |
56 | ## 1.0.1 (2018-12-09)
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hc-redux-middleware
2 |
3 | To install: `npm install --save @holochain/hc-redux-middleware`
4 |
5 | ## Usage
6 |
7 | Configure the store to use the middleware
8 |
9 | ```
10 | import { createStore, combineReducers, applyMiddleware } from 'redux'
11 |
12 | import { connect } from '@holochain/hc-web-client'
13 | import { holochainMiddleware } from '@holochain/hc-redux-middleware'
14 |
15 | // this url should use the same port set up the holochain container
16 | const url = 'ws://localhost:3000'
17 | const hcWc = connect({ url })
18 |
19 | const middleware = [holochainMiddleware(hcWc)]
20 |
21 | ...
22 |
23 | let store = createStore(reducer, applyMiddleware(...middleware))
24 | ```
25 |
26 | Lets look an example action to see how the middleware works
27 |
28 | ```
29 | {
30 | type: 'CALL_HOLOCHAIN_FUNC',
31 | payload: { ... }
32 | meta: {
33 | holochainZomeCallAction: true,
34 | instanceId: 'someInstance',
35 | zome: 'someZome',
36 | func: 'someFunc'
37 | }
38 | }
39 | ```
40 |
41 | The first thing to notice is the `meta.holochainZomeCallAction = True`. This is how the middleware detects which actions it should use to make calls to holochain.
42 |
43 | The payload will be passed directly to the call to the holochain function and must have fields that match the holochain function signature.
44 |
45 | ### Action creators
46 |
47 | To abstract away from manually creating these actions this module also provides helpers for doing that. For a particular holochain function call it will create an action creator that you can call later with parameters
48 |
49 | ```
50 | import { createHolochainZomeCallAsyncAction } from '@holochain/hc-redux-middleware'
51 |
52 | const someFuncActionCreator = createHolochainZomeCallAsyncAction('someInstance', 'someZome', 'someFunc')
53 |
54 | // later on when you want to create dispatch an action to call the function with some params
55 | const action = someFuncActionCreator.create(params)
56 | dispatch(action) // this returns a promise that resolves with the response or fails with the error
57 |
58 | ```
59 |
60 | This will autocomplete the type field using the format `someApp/someZome/someFunc`. You can also use the autogenerated `holochainAction.success().type` and `holochainAction.failure().type`
61 |
62 | It is also possible to create actions to make conductor admin calls. These are via a different action creator `createHolochainAdminAsyncAction`. These take a single call string such as `admin/instances/list` or `admin/dna/install`. See the [conductor documentation](https://developer.holochain.org/api/latest/holochain_conductor_api/interface/struct.ConductorApiBuilder.html#method.with_admin_dna_functions) for the full admin functionality. This will not be required for most hApps.
63 |
64 | ### Response Actions
65 |
66 | When a successful call to holochain is completed the middleware will dispatch an action containing the response. These actions have a type that is the same as the call but with `_SUCCESS` or `_FAILURE` appended.
67 |
68 | ```
69 | {
70 | type: 'CALL_HOLOCHAIN_FUNC_SUCCESS',
71 | payload: { ... } // this contains the function call result
72 | }
73 |
74 | {
75 | type: 'someApp/someZome/someCapability/someFunc_FAILURE',
76 | payload: { ... } // this contains details of the error
77 | }
78 | ```
79 |
80 | These can then be handler by the reducer to update the state
81 |
82 | ### Special actions
83 |
84 | There are also several actions that the middleware will dispatch automatically. Currently these are
85 |
86 | #### Typescript support
87 |
88 | We love typescript and so the action creators ship with typescript definitions and type support out of the box! The typed actions are based off the [typesafe actions](https://github.com/piotrwitek/typesafe-actions) model. Holochain action creators can be made using generics to match the function parameters and return type of the zome function
89 |
90 | ```
91 | import { createHolochainZomeCallAsyncAction } from '@holochain/hc-redux-middleware'
92 |
93 | export const someFuncActionCreator = createHolochainZomeCallAsyncAction('someApp', 'someZome', 'someFunc')
94 |
95 | // The params must match the ParamType or this will error
96 | const action = someFuncActionCreator.create(params)
97 | ```
98 | This is even more useful in the reducer.
99 |
100 | ```
101 | import { getType } from 'typesafe-actions'
102 |
103 | export function reducer (state = initialState, action: AnyAction) {
104 | switch (action.type) {
105 | case getType(someFuncActionCreator.success):
106 | // The type checker now knows that action.payload has type
107 | // set in the definition using the generic
108 | // You literally cant go wrong!
109 |
110 | ...
111 | }
112 | ```
113 | See [https://github.com/piotrwitek/typesafe-actions] for all the details of using typesafe actions
114 |
115 |
116 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@holochain/hc-redux-middleware",
3 | "version": "1.1.1",
4 | "description": "Redux middleware for seamless integration with Holochain",
5 | "main": "build/main/index.js",
6 | "typings": "build/main/index.d.ts",
7 | "module": "build/module/index.js",
8 | "files": [
9 | "build/main"
10 | ],
11 | "repository": "https://github.com/holochain/hc-redux-middleware",
12 | "license": "GPL-3.0",
13 | "keywords": [],
14 | "scripts": {
15 | "describe": "npm-scripts-info",
16 | "build": "run-s clean && run-p build:*",
17 | "build:main": "tsc -p tsconfig.json",
18 | "build:module": "tsc -p tsconfig.module.json",
19 | "fix": "run-s fix:*",
20 | "fix:prettier": "prettier \"src/**/*.ts\" --write",
21 | "fix:tslint": "tslint --fix --project .",
22 | "test": "run-s build test:*",
23 | "lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different",
24 | "test:unit": "nyc --silent ava",
25 | "version": "standard-version",
26 | "clean": "trash build test",
27 | "all": "run-s test",
28 | "prepare-release": "run-s all version"
29 | },
30 | "scripts-info": {
31 | "info": "Display information about the package scripts",
32 | "build": "Clean and rebuild the project",
33 | "fix": "Try to automatically fix any linting problems",
34 | "test": "Lint and unit test the project",
35 | "watch": "Watch and rebuild the project on save, then rerun relevant tests",
36 | "version": "Bump package.json version, update CHANGELOG.md, tag release",
37 | "prepare-release": "One-step: clean, build, test, publish docs, and prep a release"
38 | },
39 | "engines": {
40 | "node": ">=8.9"
41 | },
42 | "dependencies": {
43 | "redux": "^4.0.1",
44 | "typesafe-actions": "^2.0.4"
45 | },
46 | "devDependencies": {
47 | "@types/sinon": "^5.0.7",
48 | "ava": "1.0.0-beta.7",
49 | "codecov": "^3.1.0",
50 | "cz-conventional-changelog": "^2.1.0",
51 | "gh-pages": "^2.0.1",
52 | "npm-run-all": "^4.1.5",
53 | "nyc": "^13.1.0",
54 | "opn-cli": "^4.0.0",
55 | "prettier": "^1.15.2",
56 | "sinon": "^7.1.1",
57 | "standard-version": "^4.4.0",
58 | "trash-cli": "^1.4.0",
59 | "tslint": "^5.11.0",
60 | "tslint-config-prettier": "^1.17.0",
61 | "tslint-config-standard": "^8.0.1",
62 | "tslint-immutable": "^5.0.0",
63 | "typedoc": "^0.13.0",
64 | "typescript": "^3.1.6"
65 | },
66 | "ava": {
67 | "failFast": true,
68 | "files": [
69 | "build/main/**/*.test.js"
70 | ],
71 | "sources": [
72 | "build/main/**/*.js"
73 | ]
74 | },
75 | "config": {
76 | "commitizen": {
77 | "path": "cz-conventional-changelog"
78 | }
79 | },
80 | "prettier": {
81 | "singleQuote": true
82 | },
83 | "nyc": {
84 | "exclude": [
85 | "**/*.test.js"
86 | ]
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { holochainMiddleware } from './lib/middleware'
2 | export { createHolochainZomeCallAsyncAction, createHolochainAdminAsyncAction } from './lib/actionCreator'
3 |
--------------------------------------------------------------------------------
/src/lib/actionCreator.ts:
--------------------------------------------------------------------------------
1 | import { createAsyncAction } from 'typesafe-actions'
2 |
3 | /**
4 | *
5 | * Function that creates action creators for holochain zome function calls
6 | *
7 | */
8 | export const createHolochainZomeCallAsyncAction = (
9 | instanceId: string,
10 | zome: string,
11 | func: string
12 | ) => {
13 |
14 | const callString = [instanceId, zome, func].join('/')
15 |
16 | const action = createAsyncAction(
17 | callString,
18 | callString + '_SUCCESS',
19 | callString + '_FAILURE')
20 | ()
21 |
22 | const newAction = action as (typeof action & {
23 | create: (param: ParamType) => any,
24 | sig: (param: ParamType) => Promise
25 | })
26 |
27 | // the action creators that are produced
28 | newAction.create = (params: ParamType) => {
29 | return {
30 | type: callString,
31 | meta: {
32 | holochainZomeCallAction: true,
33 | instanceId,
34 | zome,
35 | func
36 | },
37 | payload: params
38 | }
39 | }
40 |
41 | return newAction
42 | }
43 |
44 | /**
45 | *
46 | * Function that creates action creators for holochain conductor admin calls
47 | *
48 | */
49 | export const createHolochainAdminAsyncAction = (
50 | ...segments: Array
51 | ) => {
52 |
53 | const callString = segments.length === 1 ? segments[0] : segments.join('/')
54 |
55 | const action = createAsyncAction(
56 | callString,
57 | callString + '_SUCCESS',
58 | callString + '_FAILURE')
59 | ()
60 |
61 | const newAction = action as (typeof action & {
62 | create: (param: ParamType) => any,
63 | sig: (param: ParamType) => Promise
64 | })
65 |
66 | // the action creators that are produced
67 | newAction.create = (params: ParamType) => {
68 | return {
69 | type: callString,
70 | meta: {
71 | holochainAdminAction: true,
72 | callString
73 | },
74 | payload: params
75 | }
76 | }
77 |
78 | return newAction
79 | }
80 |
--------------------------------------------------------------------------------
/src/lib/middleware.test.ts:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import sinon from 'sinon'
3 |
4 | import { holochainMiddleware } from './middleware'
5 | import { createHolochainZomeCallAsyncAction, createHolochainAdminAsyncAction } from './actionCreator'
6 |
7 | const mockWebClient = (callResponse: string) => Promise.resolve({
8 | call: (callStr: string) => (params: any) => {
9 | return Promise.resolve(callResponse)
10 | },
11 | callZome: (instanceId: string, zome: string, func: string) => (params: any) => {
12 | return Promise.resolve(callResponse)
13 | },
14 | close: () => Promise.resolve('closed'),
15 | ws: {
16 | subscribe: sinon.spy(),
17 | on: sinon.spy()
18 | }
19 | })
20 |
21 | const create = (callResponse: any) => {
22 | const store = {
23 | getState: sinon.spy(() => ({})),
24 | dispatch: sinon.spy()
25 | }
26 | const next = sinon.spy()
27 | const invoke = (action: any) => holochainMiddleware(mockWebClient(callResponse))(store)(next)(action)
28 |
29 | return { store, next, invoke }
30 | }
31 |
32 | test('It passes non-holochain actions to the next reducer', async t => {
33 | let { next, invoke } = create('')
34 |
35 | const nonHolochainAction = { type: 'not-holochain-action' }
36 | await invoke(nonHolochainAction)
37 |
38 | t.true(next.calledWith(nonHolochainAction))
39 | })
40 |
41 | test('It passes holochain actions and dispatches new action on success. Ok is unwrapped ', async t => {
42 | let { next, invoke, store } = create(JSON.stringify({ Ok: 'success' }))
43 |
44 | const holochainAction = createHolochainZomeCallAsyncAction('happ', 'zome','func')
45 | const result = await invoke(holochainAction.create({}))
46 |
47 | t.deepEqual(result, 'success')
48 | t.true(next.calledWith(holochainAction.create({})))
49 | t.true(store.dispatch.calledWith(holochainAction.success('success')))
50 | })
51 |
52 | test('It passes holochain actions and dispatches new error action on holochain error. Err is unwrapped ', async t => {
53 | let { next, invoke, store } = create(JSON.stringify({ Err: 'fail' }))
54 |
55 | const holochainAction = createHolochainZomeCallAsyncAction('happ', 'zome','func')
56 |
57 | try {
58 | await invoke(holochainAction.create({}))
59 | } catch (result) {
60 | t.deepEqual(result, Error('fail'))
61 | t.true(next.calledWith(holochainAction.create({})))
62 | t.deepEqual(store.dispatch.lastCall.args[0], holochainAction.failure(Error('fail')))
63 | }
64 | })
65 |
66 | test('It passes holochain actions and dispatches new action on success. Raw return is passed directly ', async t => {
67 | let { next, invoke, store } = create(JSON.stringify({ someField: 'success' }))
68 |
69 | const holochainAction = createHolochainZomeCallAsyncAction('happ', 'zome','func')
70 | const result = await invoke(holochainAction.create({}))
71 |
72 | t.deepEqual(result, { someField: 'success' })
73 | t.true(next.calledWith(holochainAction.create({})))
74 | t.true(store.dispatch.calledWith(holochainAction.success({ someField: 'success' })))
75 | })
76 |
77 | test('can accept container admin style responses which return unstringified json objects', async t => {
78 | let { next, invoke, store } = create({ someField: 'success' })
79 |
80 | const holochainAction = createHolochainAdminAsyncAction('admin', 'dna', 'list')
81 | const result = await invoke(holochainAction.create({}))
82 |
83 | t.deepEqual(result, { someField: 'success' })
84 | t.true(next.calledWith(holochainAction.create({})))
85 | t.true(store.dispatch.calledWith(holochainAction.success({ someField: 'success' })))
86 | })
87 |
88 | test('can accept container admin style responses which return unstringified json arrays', async t => {
89 | let { next, invoke, store } = create(['item1', 'item2'])
90 |
91 | const holochainAction = createHolochainAdminAsyncAction('admin', 'dna', 'list')
92 | const result = await invoke(holochainAction.create({}))
93 |
94 | t.deepEqual(result, ['item1', 'item2'])
95 | t.true(next.calledWith(holochainAction.create({})))
96 | t.true(store.dispatch.calledWith(holochainAction.success(['item1', 'item2'])))
97 | })
98 |
99 | test('can accept container admin style responses which return plain strings which cannot be parsed to JSON', async t => {
100 | let { next, invoke, store } = create('test string')
101 |
102 | const holochainAction = createHolochainAdminAsyncAction('admin', 'instance', 'list')
103 | const result = await invoke(holochainAction.create({}))
104 |
105 | t.deepEqual(result, 'test string')
106 | t.true(next.calledWith(holochainAction.create({})))
107 | t.true(store.dispatch.calledWith(holochainAction.success('test string')))
108 | })
109 |
--------------------------------------------------------------------------------
/src/lib/middleware.ts:
--------------------------------------------------------------------------------
1 | import { Middleware, AnyAction } from 'redux'
2 |
3 | type hcWebClientConnect = Promise<{
4 | call: (callStr: string) => (params: any) => Promise,
5 | callZome: (instance: string, zome: string, func: string) => (params: any) => Promise,
6 | close: () => Promise,
7 | ws: any
8 | }>
9 |
10 | export const holochainMiddleware = (hcWc: hcWebClientConnect): Middleware => store => {
11 | // stuff here has the same life as the store!
12 | // this is how we persist a websocket connection
13 |
14 | const connectPromise = hcWc.then(({ call, callZome, ws }) => {
15 | store.dispatch({ type: 'HOLOCHAIN_WEBSOCKET_CONNECTED' })
16 |
17 | ws.on('close', () => {
18 | store.dispatch({ type: 'HOLOCHAIN_WEBSOCKET_DISCONNECTED' })
19 | })
20 |
21 | return { call, callZome }
22 | })
23 |
24 | return next => (action: AnyAction) => {
25 | if (action.meta && (action.meta.holochainZomeCallAction || action.meta.holochainAdminAction)) {
26 | next(action) // resend the original action so the UI can change based on requests
27 |
28 | return connectPromise.then(({ call, callZome }) => {
29 |
30 | let callFunction
31 | if (action.meta.holochainZomeCallAction) {
32 | const { instanceId, zome, func } = action.meta
33 | callFunction = callZome(instanceId, zome, func)
34 | } else {
35 | callFunction = call(action.meta.callString)
36 | }
37 |
38 | return callFunction(action.payload)
39 | .then((rawResult: string) => {
40 |
41 | // holochain calls will strings (possibly stringified JSON)
42 | // while container admin calls will return parsed JSON
43 | let result
44 | try {
45 | result = JSON.parse(rawResult)
46 | } catch (e) {
47 | result = rawResult
48 | }
49 |
50 | if (result.Err !== undefined) { // holochain error
51 | store.dispatch({
52 | type: action.type + '_FAILURE',
53 | payload: result.Err
54 | })
55 | return Promise.reject(Error(result.Err))
56 | } else if (result.Ok !== undefined) { // holochain Ok
57 | store.dispatch({
58 | type: action.type + '_SUCCESS',
59 | payload: result.Ok
60 | })
61 | return result.Ok
62 | } else { // unknown. Return raw result as success
63 | store.dispatch({
64 | type: action.type + '_SUCCESS',
65 | payload: result
66 | })
67 | return result
68 | }
69 | })
70 | .catch((err: Error) => { // websocket error
71 | store.dispatch({
72 | type: action.type + '_FAILURE',
73 | payload: err
74 | })
75 | return Promise.reject(err)
76 | })
77 | })
78 | } else {
79 | return next(action)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "outDir": "build/main",
5 | "rootDir": "src",
6 | "moduleResolution": "node",
7 | "module": "commonjs",
8 | "declaration": true,
9 | "inlineSourceMap": true,
10 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
11 |
12 | // "strict": true /* Enable all strict type-checking options. */,
13 |
14 | /* Strict Type-Checking Options */
15 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
16 | // "strictNullChecks": true /* Enable strict null checks. */,
17 | // "strictFunctionTypes": true /* Enable strict checking of function types. */,
18 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
19 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
20 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
21 |
22 | /* Additional Checks */
23 | "noUnusedLocals": true /* Report errors on unused locals. */,
24 | // "noUnusedParameters": true /* Report errors on unused parameters. */,
25 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
26 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
27 |
28 | /* Debugging Options */
29 | "traceResolution": false /* Report module resolution log messages. */,
30 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */,
31 | "listFiles": false /* Print names of files part of the compilation. */,
32 | "pretty": true /* Stylize errors and messages using color and context. */,
33 |
34 | /* Experimental Options */
35 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
36 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
37 |
38 | "lib": ["es2017"],
39 | "types": [],
40 | "typeRoots": ["node_modules/@types", "src/types"]
41 | },
42 | "include": ["src/**/*.ts"],
43 | "exclude": ["node_modules/**"],
44 | "compileOnSave": false
45 | }
46 |
--------------------------------------------------------------------------------
/tsconfig.module.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "target": "esnext",
5 | "outDir": "build/module",
6 | "module": "esnext"
7 | },
8 | "exclude": [
9 | "node_modules/**"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint-config-standard"],
3 | "linterOptions": {
4 | "exclude": [
5 | "_test.ts",
6 | "config/**/*.js",
7 | "node_modules/**/*.ts",
8 | "coverage/lcov-report/*.js"
9 | ]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------