5.8 KiB
Guidelines
MR/Commit Messages
NOTE: Not necessary to follow this until V1.0 release.
Use a Conventional Commit format when creating MR's. For example:
feat(Primitives): Add Transaction Primitive
... or to indicate a breaking change (using !):
chore!: Drop support for Node 16
This will ensure that we can automatically generate correct Semantic Versions and changelogs between releases (using Lerna).
A table of common prefixes (conventions) is below:
| Change | Description |
|---|---|
| feat | A new feature |
| fix | A bug fix |
| docs | Documentation only changes |
| style | Changes that do not affect the meaning of the code (formatting) |
| refactor | A code change that neither fixes a bug nor adds a feature |
| perf | A code change that improves performance |
| test | Adding missing tests or correct existing tests |
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) |
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) |
| chore | Other changes that don't modify src or test files |
| revert | Reverts a previous commit |
Patterns
Dependency Injection
We use Dependency Injection (DI) for several key reasons:
- Improved testability: Dependencies can be easily mocked during unit tests
- Reduced coupling: Classes depend on abstractions rather than concrete implementations
- Greater flexibility: Implementations can be swapped without modifying dependent classes
- Clearer dependencies: Constructor signatures explicitly document required dependencies
- Easier refactoring: Component relationships are clearly defined
- Simpler dependency management: Easier control over resource initialization and lifecycle
Try not to over-abstract, but if there is already a Base Class (or interface) available, always try to use that (via dependency injection) in favor of a concrete implementation.
For example:
class SomeClass {
protected blockchain: BaseBlockchain;
protected store: BaseStore;
constructor(
// Dependency Injection first
dependencies: {
blockchain: BaseBlockchain,
store: BaseStore,
},
// Then main (required) initialization params
initParams,
// Then optional params (if applicable).
opts,
) {
this.blockchain = dependencies.blockchain;
this.store = dependencies.store;
}
}
Factory Patterns
When creating classes, create static from(...) (or fromSpecificThing(...)) methods.
In Javascript, a constructor cannot be asynchronous and we want to ensure that we can await class instantiation to mitigate against stateful errors.
For example:
class SomeClass {
// Static "from" factory method.
// NOTE: The below args are shorthand for using the same arguments as the constructor.
static async from(...args: ConstructorParameters<typeof SomeClass>) {
// Create the instance.
const someClass = new SomeClass(...args);
// Execute the asynchronous initialization function.
await someClass.start();
// Return the instance.
return someClass;
}
}
Although this isn't always necessary (i.e. not all instantiations need to be async), it keeps instantiation of classes consistent.
Semantically, think of "from" as meaning "instantiate from": This is more succinct than a createFrom... factory method which, if we have multiple ways of instantiation, can become verbose. E.g. PrivateKey.fromWIF() as opposed to PrivateKey.createFromWIF(). It also allows us to differentiate between actually "creating" something (e.g. a Filesystem.createFile(...)) versus "instantiating" a representation of a "thing".
Validations
Check types with TypeScript - Runtime type-checking is a code smell. If the function accepts a string, assume it's been given a string. TypeScript definitions should expose improperly called functions to the developer at compile time, don't re-implement it at runtime.
There are a few exceptions to this:
- If TypeScript's lack of dependant types makes validating certain properties of a type (e.g. the length of a Uint8Array), then runtime type-checking should be used to detect and return errors.
- If we are working with a network (remote/untrusted) payload, Type Validation should be done with Zod.
An example of working with Zod:
// Define a schema so that we can validate the payload.
export const SomeRemotePayload = z.object({
publicKey: z.string(),
action: z.enum(['set', 'delete', 'clear']),
key: z.string(),
value: z.string(),
timestamp: z.number(),
});
// Run Zod's validation against this payload.
// NOTE: This will throw if it fails.
const parsedPayload = SomeRemotePayload.parse(rawPayload);
// (Optional) If this type is needed elsewhere, we can infer it from Zod and export it.
export type StoreRemoteHTTPEvent = z.infer<typeof StoreRemoteHTTPEventSchema>;