-
Notifications
You must be signed in to change notification settings - Fork 490
feat: introduce function support for deploy lifecycle hooks #1101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Ah, this needs a rebase. |
| if (typeof self.config.contractsConfig.afterDeploy === 'function') { | ||
| this.getAfterDeployLifecycleHookDependencies().then(dependencies => { | ||
| (async () => { | ||
| await self.config.contractsConfig.afterDeploy(dependencies); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notice that we always call lifecycle hook within async/await context.
This also means that lifecycle hooks have to either (optionally) return a Promise, or being written in async/await themselves, to notify Embark that a hook is done
@alaibe I looked into supporting both, promise support as well as callback support, unfortunately, I couldn't make both APIs work, without the user worrying about how their lifecycle hooks are being called.
I also believe, users shouldn't be needed to do both at the same time (calling callback and returning promise).
So I decided to have them use either Promise APIs or async/await. This seems natural to me as well (especially because this is a new API and I don't think there's a good reason to keep callback based APIs around)
| if (typeof cmd === 'function') { | ||
| this.getOnDeployLifecycleHookDependencies(contract).then(dependencies => { | ||
| const callbackFn = (value) => { | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a left over.
| return cb(new Error("error running afterDeploy")); | ||
| } | ||
| if (typeof self.config.contractsConfig.afterDeploy === 'function') { | ||
| this.getAfterDeployLifecycleHookDependencies().then(dependencies => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth putting the whole function in async/await as well, so that getAfterDeployLIfecyclehookDependencies() can be called with await, meaning also that afterDeploy() can be called with await without wrapping it in an IIFE.
| }); | ||
| } | ||
|
|
||
| getOnDeployLifecycleHookDependencies(contractConfig) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be rewritten using async/await
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I've realised those aren't easily converted to async/await because our entire event bus is callback-based.
| }); | ||
| } | ||
|
|
||
| getAfterDeployLifecycleHookDependencies() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be rewritten using async/await
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. Turns out we can't really convert this to async/await as there's not promise based API used within this function.
|
|
||
| self.contractDependencies[className] = self.contractDependencies[className] || []; | ||
|
|
||
| if (contract.deps && contract.deps.length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Array.isArray(contract.deps) same technique as before, seems more elegant?
| return cb(new Error(`Error when registering onDeploy hook for ${contract.name}: ${err.message}`)); | ||
| }); | ||
| } else { | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cosmetic: no extra line here?
| if (typeof cmd === 'function') { | ||
| this.getOnDeployLifecycleHookDependencies(contract).then(dependencies => { | ||
| const callbackFn = (value) => { | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cosmetic: can be one line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is actually a left over that has been removed in the meantime :)
| }); | ||
| } | ||
|
|
||
| getAfterDeployLifecycleHookDependencies() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getOnDeployLifecycleHookDependencies and getAfterDeployLifecycleHookDependencies seems to share some logic, do you think it could be possible to extract the sharing logic into function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I was considering that too. Especially the part that puts together the dependencies ( think the rest is already pretty broken down into pieces and now really reusable).
But then I thought, it might be a little overkill to introduce a new function for:
this.events.request('blockchain:get', web3 => {
let dependencies = { contracts: {}, web3 };
contractInstances.forEach(contractInstance => {
dependencies.contracts[contractInstance.className] = contractInstance.instance;
});
resolve(dependencies);
});
Anyways, will update it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To reduce the shared code and also not having to re-create the contract object every time there is a new onDeploy, I think we should have one of the contract modules have a list of the web3 contracts an retrieve them here with an event.
Every time a contract is deployed, it would create that contract object. That would be quite useful for a couple of our modules. I'm thinking about this one, the ENS one, maybe even tests.
Maybe one of our modules already does that, but I don't recall. Also, maybe there is a good reason we didn't do that before (maybe a memory issue)? What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very good point you're raising here @jrainville
From the top of my head I'd say ContractsManager should be responsible for that. re: Memory issues: AFAIK node has a max memory limit of ~1.5 GB by default but can be easily extended if needed.
I guess that's something we'll have to experiment with.
I believe this would be a good candidate for a separate PR/effort though.
204b001 to
c9e445e
Compare
| dependencies.contracts[contractInstance.className] = contractInstance.instance; | ||
| }); | ||
| return dependencies; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alaibe this is the handler that's shared in both get*Dependencies() functions.
Notice that that's the only part that could potentially be shared.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, can I suggest using reduce? That will be more declarative
7abf485 to
7452df5
Compare
|
Documentation for this feature can be found here: embark-framework/embark-site#106 |
| } | ||
|
|
||
| if (onDeploy && onDeploy.length) { | ||
| if (Array.isArray(onDeploy)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it have issues with empty arrays?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was actually the reason why I didn't use isArray in the first place, however, if it happens to be an empty array, it's fine as the following code path will basically be a noop.
| if (Array.isArray(contract.deps)) { | ||
| contract.deps.forEach(name => { | ||
| self.contractDependencies[className].push(name); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can just do a concat self.contractDependencies[className] = self.contractDependencies[className].concat(contract.deps);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
|
|
||
| getOnDeployLifecycleHookDependencies(contractConfig) { | ||
| let dependencyNames = contractConfig.deps || []; | ||
| dependencyNames.push(contractConfig.className); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the same contract can be added multiple times to the list like this. I don't think it's a super big deal, since it's only references, but we never know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, we can dedupe dependencyNames before we enter the async.map()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deduped
| async.map(dependencyNames, (contractName, next) => { | ||
| this.events.request('contracts:contract', contractName, (contractRecipe) => { | ||
| if (!contractRecipe) { | ||
| next(new Error('ReferredContractDoesNotExist')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you put the name of the referred contract too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
| } else if (!result) { | ||
| self.logger.info(params.contract.className + ' deployIf directive returned false; contract will not deploy'); | ||
| params.shouldDeploy = false; | ||
| reject(err); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you don't have to return here, but I feel like it's safer to anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, no return statement needed for resolve/reject as this will return the promise immediately.
| }); | ||
| } | ||
|
|
||
| getAfterDeployLifecycleHookDependencies() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To reduce the shared code and also not having to re-create the contract object every time there is a new onDeploy, I think we should have one of the contract modules have a list of the web3 contracts an retrieve them here with an event.
Every time a contract is deployed, it would create that contract object. That would be quite useful for a couple of our modules. I'm thinking about this one, the ENS one, maybe even tests.
Maybe one of our modules already does that, but I don't recall. Also, maybe there is a good reason we didn't do that before (maybe a memory issue)? What do you think?
Prior to this commits deployment lifecycle hooks had been defined as Array<string> due
to historical reasons (contracts.js) used to be a json file back in the days.
`deployIf`, `onDeploy` and `afterDeploy` can now be defined as (async)
function and have access to several dependencies such as contract instances and web3.
However, in order to have needed dependencies registered in the dependencies object,
all lifecycle hook dependencies need to be listed in the `deps` property
as shown below.
Also note that this is NOT a breaking change. Existing deployment lifecycle
hooks written in Array<string> style still work.
All three lifecycle hooks can now be defined as (async) functions and get an dependency
object with a shape like this:
```
interface DeploymentLifecycleHookDependencies {
contracts: Map<string, ContractInstance>;
web3: Web3Instance
}
```
`deployIf` lifecycle hook has to return a promise (or be defined using async/await and return
a value) like this:
```
contracts: {
MyToken: {...},
SimpleStorage: {
deps: ['MyToken'], // this is needed to make `MyToken` available within `dependencies`
deployIf: async (dependencies) => {
return dependencies.contracts.MyToken_address;
}
},
}
```
Vanilla promises (instead of async/await) can be used as well:
```
contracts: {
MyToken: {...},
SimpleStorage: {
deps: ['MyToken'],
deployIf: (dependencies) => {
return new Promise(resolve => resolve(dependencies.contracts.MyToken_address);
}
},
}
```
`onDeploy` as well, returns either a promise or is used using async/await:
```
contracts: {
SimpleStorage: {
onDeploy: async (dependencies) => {
const simpleStorage = dependencies.contracts.SimpleStorage;
const value = await simpleStorage.methods.get().call();
console.log(value);
}
},
}
```
`afterDeploy` has automatically access to all configured and deployed contracts of the dapp:
```
contracts: {
SimpleStorage: {...},
MyToken: {...},
afterDeploy: (dependencies) => {
console.log('Done!');
}
}
```
Closes #1029
7452df5 to
77cc23b
Compare
Prior to this commits deployment lifecycle hooks had been defined as Array due
to historical reasons (contracts.js) used to be a json file back in the days.
deployIf,onDeployandafterDeploycan now be defined as (async)function and have access to several dependencies such as contract instances and web3.
However, in order to have needed dependencies registered in the dependencies object,
all lifecycle hook dependencies need to be listed in the
depspropertyas shown below.
Also note that this is NOT a breaking change. Existing deployment lifecycle
hooks written in Array style still work.
All three lifecycle hooks can now be defined as (async) functions and get an dependency
object with a shape like this:
deployIflifecycle hook has to return a promise (or be defined using async/await and returna value) like this:
Vanilla promises (instead of async/await) can be used as well:
onDeployas well, returns either a promise or is used using async/await:afterDeployhas automatically access to all configured and deployed contracts of the dapp:Closes #1029