XO Integration Part II
This commit is contained in:
@@ -54,6 +54,7 @@ For example:
|
||||
|
||||
```ts
|
||||
class SomeClass {
|
||||
// NOTE: We do not use concrete implementations (e.g. "BlockchainELectrum" or "StoreIndexedDB"), but the abstractions ("BaseBlockchain" and "BaseStore") instead.
|
||||
protected blockchain: BaseBlockchain;
|
||||
protected store: BaseStore;
|
||||
|
||||
@@ -86,6 +87,8 @@ For example:
|
||||
class SomeClass {
|
||||
// Static "from" factory method.
|
||||
// NOTE: The below args are shorthand for using the same arguments as the constructor.
|
||||
// Do not feel forced to use this - there are definitely some situations where you will have to define the args manually.
|
||||
// But do try to follow the argument patterns in the above "Dependency Injection" section if possible.
|
||||
static async from(...args: ConstructorParameters<typeof SomeClass>) {
|
||||
// Create the instance.
|
||||
const someClass = new SomeClass(...args);
|
||||
@@ -103,6 +106,85 @@ Although this isn't always necessary (i.e. not all instantiations need to be `as
|
||||
|
||||
*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".*
|
||||
|
||||
#### Extending from Static Factory Methods
|
||||
|
||||
If you anticipate that someone will `extend` a class and want to re-use your static factory method, this can be achieved as follows:
|
||||
|
||||
```ts
|
||||
class YourClass {
|
||||
// NOTE: The generic T extends typeof YourClass (so that we can return the correct type).:
|
||||
static async from<T extends typeof YourClass>(
|
||||
// NOTE: "this" as first argument is what technically, allows the extended class to instantiate correctly.
|
||||
this: T,
|
||||
params: SomeParams = {},
|
||||
) {
|
||||
// NOTE: So that Typescript can type correctly, we perform a cast.
|
||||
return new this(params) as InstanceType<T>;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Extended classes can then be instantiated seamlessly as follows:
|
||||
|
||||
```ts
|
||||
class YourExtendedClass extends YourClass {
|
||||
// ...
|
||||
}
|
||||
|
||||
// This will now be an instance of YourExtendedClass without having to redefine the `from(...)` method.
|
||||
const yourExtClass = YourClass.from(...);
|
||||
```
|
||||
|
||||
This adds quite a bit of confusing syntactic overhead, so only do this if you anticipate use-cases where extension is likely.
|
||||
|
||||
### EventEmitter Patterns for BaseAdapters
|
||||
|
||||
All BaseAdapters should extend EventEmitter. This design choice enables Stack components to work seamlessly with different frontend frameworks (vanilla JS, Vue, React, etc.) through a consistent event-based interface.
|
||||
|
||||
1. State Management
|
||||
Adapters in Stack should maintain minimal state. While backend flows typically operate procedurally (fetching data as needed), frontends often require reactive state updates for UI rendering.
|
||||
|
||||
2. Framework Agnostic
|
||||
Since reactive state handling varies across frameworks, we use event hooks rather than imposing specific state management patterns, allowing applications to handle higher-level state according to their framework (and product's) conventions.
|
||||
|
||||
To illustrate how this might be used with VueJS:
|
||||
|
||||
```ts
|
||||
// Create a reactive variable (Vue example)
|
||||
const isConnected = ref();
|
||||
|
||||
// Initialize adapter
|
||||
const someAdapter = new SomeAdapter();
|
||||
|
||||
// Bind adapter state to reactive variable
|
||||
someAdapter.on('isConnected', (isConnected) => {
|
||||
isConnected.value = isConnected;
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// Declare a reactive variable for unspents (Vue example)
|
||||
const unspents = ref([]);
|
||||
|
||||
// Initialize the wallet
|
||||
const someWallet = await WalletHD.from(genesisData);
|
||||
|
||||
// Hook into state updated event
|
||||
someWallet.on(
|
||||
'stateUpdated',
|
||||
async () => {
|
||||
// Sync unspents to the reactive variable
|
||||
unspents.value = await someWallet.updateUnspents(),
|
||||
},
|
||||
// (Optional) Debounce for 500ms to prevent excessive redraws
|
||||
500
|
||||
);
|
||||
```
|
||||
|
||||
**Note: While XO Wallet will provide convenience wrappers around these patterns, this represents the general approach to use in @xocash/stack.**
|
||||
|
||||
### 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.
|
||||
@@ -112,6 +194,8 @@ 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).
|
||||
|
||||
#### Validating External Payloads
|
||||
|
||||
An example of working with Zod:
|
||||
|
||||
```ts
|
||||
@@ -132,4 +216,19 @@ const parsedPayload = SomeRemotePayload.parse(rawPayload);
|
||||
export type StoreRemoteHTTPEvent = z.infer<typeof StoreRemoteHTTPEventSchema>;
|
||||
```
|
||||
|
||||
#### Leverage Primitives Over Repeated Validation
|
||||
|
||||
Avoid revalidation at every code-path: This is not only inefficient, but a giant code-smell. Instead, make use of Primitives as these provide assurances of correctness (Primitives are not just intended as a convenience - they are also intended to provide safety). For example, given a method:
|
||||
|
||||
|
||||
```ts
|
||||
// You can accept other types (this is a nice convenience).
|
||||
broadcastTransaction(transaction: Transaction | Uint8Array): Transaction {
|
||||
// ... but always cast to the Primitive (to ensure correctness).
|
||||
if(!(transaction instanceof Transaction) {
|
||||
transaction = Transaction.from(transaction);
|
||||
}
|
||||
|
||||
// ... use the Transaction primitive in follow up code.
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user