# 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) { // 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; ```