Thank you for wanting to contribute to the @paychex/core
library!
Please read this entire file before pushing any code to the repository.
Please ensure you do the following before submitting any code for review:
- Reach out to at least one repository maintainer early in development to assist with design decisions.
- Use Gitflow to name your branches. For normal contributions, that means creating a
feature/<name>
orbugfix/<name>
branch off of thedevelop
branch. - Ensure your test coverage is at 100%.
- Write documentation for any public methods.
- Use conventional commit messages on all commits.
Finally, because your code will be consumed by many other developers, please allow enough time for the maintainers to review your proposed changes thoroughly.
Try to follow the conventional commits standard. The following commit types should be used:
type | description |
---|---|
feat | The commit adds a new feature. |
fix | The commit fixes a bug. |
perf | The commit is solely to improve performance. |
docs | The commit only updates or improves documentation. |
test | The commit only modifies tests. |
refactor | The commit changes code but does not fix a bug or modify public methods. |
chore | The commit modifies builds, publishing, source control, or other non-code functionality. |
perf(signals): reuse constructor function
Rather than create a new constructor function on each invocation of
ready(), we now reuse an existing function. This also makes the logic in
autoReset() a bit more readable.
feat(stores): remove asObservable wrapper
This functionality can be provided by Store implementers more simply,
and likely better aligned with feature use cases. It also removes a
significant size dependency on RxJS.
BREAKING
Make sure your public methods and types are all documented. Use jsDocs and run npm run docs
to ensure your documentation compiles. Include examples of typical use cases.
This repository contains code that will be used by many developers. Accordingly, you should aim for 100% code coverage of any features you write. Each conditional branch should be tested, edge cases should be considered, and errors should be propagated appropriately.
Code written for a library is different from application code. A library provides a toolbox for other developers. And much like a screwdriver or a hammer, library code is completely unaware of how it will be used. Also like a tool, each feature in a library should serve a single well-defined purpose.
The design principles most important to code in this repo are:
Please review the above links and make sure you feel comfortable with the general ideas.
To ensure we follow the above principles, every function the @paychex/core
library can be categorized as one of the following:
- feature function
- factory function
- wrapper function
The feature function
is the building block of good code. It follows the Single Responsibility Principle, meaning it accomplishes one logical operation. Examples of feature functions include:
- get user information
- start timing an operation
- activate the next item in a list
If a function has more than 1 logical purpose then it is too big and should be split up. We will see an example of that later in this document.
A factory function
is a type of feature function. Its job is to create an object (or a function).
The following methods in this repo are all factory functions:
modelList()
eventBus()
createTracker()
createDataLayer()
createProxy()
A wrapper function
is also a type of feature function. And like a factory function, its job is to create a new instance of an item. But more specifically, it is the mechanism we use to extend the behavior of existing objects and functions. We will see examples of wrapper functions later in this document.
Each Store instance is solely responsible for coordinating with its persistence mechanism. Functionality such as encryption and key-prefixing (while important and perhaps even required by all Stores) has been separated from the Store implementations. This enables each Store to focus on doing one thing as well as it can.
If you have written your feature following SRP and O-C principles, you should be able to extend (not modify) your base feature quite easily using the following design patterns:
- Proxy: wraps an object and returns the same interface
- Adapter: wraps an object and returns a different interface
- Decorator: wraps an object and adds methods to the interface
To assist consumers, the following naming convention should apply to your extension methods: If you are narrowing or changing the interface, name your method as<Feature>
; if you are returning the same or expanded interface, name your method with<Feature>
. In other words, Proxy and Delegate wrappers typically start with with
while Adapter wrappers start with as
.
IMPORTANT: Wrapper methods must never modify the original, wrapped object. Imagine if withEncryption()
modified the underlying indexedDB()
store -- all other consumers of the store would receive encryption even though they didn't ask for it. New code should not change the behavior of existing code.
To summarize, wrapper methods should:
- appropriately name the feature they are adding to the interface
- accept an implementation of the interface as their first argument
- never mutate the delegated implementation
- (optionally) accept configuration information necessary for the feature
One simple way to ensure delegates are not mutated is to use the object spread syntax:
export function withUppercase(delegate) {
return {
// spread ensures the same API is available on
// our Proxy object as on the inner delegate
...delegate,
// redefining wrappedMethod below means it will
// be used in place of the delegate's original
// wrappedMethod that was spread above
/**
* uppercases the arg to wrappedMethod
*/
wrappedMethod(arg) {
const upper = String(arg).toUpperCase();
return delegate.wrappedMethod(upper);
}
};
}
If wrapping a function, you can use the following template:
export function withSomeNewFeature(fn) {
return function newFeature(...args) {
// we can modify the args or pass them
// unchanged to the original function:
const result = fn(...args);
// we can now modify the result however
// we wish before returning it:
return String(result).toUpperCase();
};
}
Now that we know how the wrapper methods should work, let's examine each design pattern in detail.
A proxy is an object that has the same interface as another object and is used in place of that other object. It provides a surrogate or placeholder for another object to control access to it. It intends to add a wrapper and delegation to protect the real component from undue complexity.
Proxy methods in @paychex/core
can be identified by their name. Each starts with the prefix 'with'
:
withEncryption
withPrefix
withNesting
In general, the Proxy methods in @paychex/core
have the following signature:
export function withFeature( delegate:IDelegate [, options:{[string]: any}] ): IDelegate
It's the returned implementation that consumers will access. It's the Proxy's job to determine when and how it should access the delegate's public members. See the code for examples of how various @paychex/core
proxy methods work with their delegates.
An adapter allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code.
Adapter methods in @paychex/core
can be identified by their name. Each starts with the prefix 'as'
:
asResponseCache
For example, the asResponseCache
adapter method wraps a Store
implementation so it satisfies the Cache
interface, allowing a Store to be used to persist data layer Response
objects.
In general, the Adapter methods in @paychex/core
have the following signature:
export function asFeature( delegate:IDelegate [, options:{[string]: any}] ): IOtherInterface
A decorator modifies the surface API of a single object, often by adding new functionality.
Decorator methods in @paychex/core
can be identified by their name. Like Proxies, each decorator wrapper starts with the prefix 'with'
:
withOrdering
For example, the withOrdering
decorator method wraps a ModelList
implementation to add an orderBy
method. This is a decorator because it extends the underlying ModelList interface with new methods, expanding the public API.
In general, the Decorator methods in @paychex/core
have the following signature:
export function withFeature( delegate:IDelegate [, options:{[string]: any}] ): IDelegate & IOtherInterface
Apply single-responsibility and open-closed principles to our code allows us to provide new features easily through proxy
, adapter
, and decorator
wrapper methods.