132 lines
5.7 KiB
Markdown
132 lines
5.7 KiB
Markdown
# Guidelines
|
|
|
|
## MR/Commit Messages
|
|
|
|
NOTE: Not necessary to follow this until V1.0 release.
|
|
|
|
Use a [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) 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](https://semver.org/) 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:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
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](https://www.npmjs.com/package/zod).
|
|
|
|
An example of working with Zod:
|
|
|
|
```ts
|
|
// 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>;
|
|
```
|
|
|
|
|