5.7 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(
//
dependencies: {
blockchain: BaseBlockchain,
store: BaseStore,
}
) {
this.blockchain = dependencies.blockchain;
this.store = dependencies.store;
}
}
Factory Patterns
When creating classes, create static from(...) (or fromSpecificThing(...)) methods.
Semantically, think of "from" as meaning "instantiate from": This is more succinct than a create 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".
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.
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>;