with-domain-driven-caching
v1.1.3
Published
A wrapper that makes it simple to add caching to any function
Downloads
63
Maintainers
Readme
with-domain-driven-caching
safe 🔭, easy ✨, and powerful ⚡ domain.logic.query caching, via domain.object definitions
overview
wasting time writing cache updates manually on the frontend? got slow queries due to fan out effects on the backend? want to move faster without adhoc code and data stores?
safely and easily add dynamodb, s3, localstorage, or any other performant persistance layer backed caching to any domain.logic.query by leveraging the domain.object definitions you're already writing
features domain driven automation to keep your cache fresh and clean
- ⚡ intuitive pit-of-success interface
- ✨ automatic cache updates
- ✨ automatic cache invalidations
- 🔭 observable cache operations
install
npm install with-domain-driven-caching
use
⚡ add caching to a query
easily add caching to any domain.logic.query with the wrapper
import { createCache } from 'simple-dynamodb-cache';
import { withQueryCaching } from 'with-domain-driven-caching';
// say you want to cache in dynamodb
const cache = createCache({ table: config.dynamodb.cache });
// say you have this query
const getJobByUuid = async ({ uuid }): Promise<Job> => { /* ... */ }
// add caching to it easily, like so
const cached = withQueryCaching(getJobByUuid, { cache })
✨ automatically update cache on mutation
if your query outputs domain objects, you've already got automatic cache updates enabled
for example
// say you have a query with caching that returns a job by uuid
const getJobByUuid = withQueryCaching(async ({ uuid }): Promise<Job> => { /* ... */ }, { cache })
// lets lookup the state of a job we know exists
const jobUuid = '__some_uuid__';
const jobBefore = await getJobByUuid({ uuid: jobUuid }); // cache.miss -> query.output was set to cache
expect(jobBefore.name).toEqual('Junk Removal');
// say you have a mutation which updates the status of a job and returns the new job state
const setJobName = withMutationEffects(async ({ jobUuid, name }): Promise<Job> => { /* ... */ }, { cache });
// lets update the status of our job
await setJobName({ jobUuid, name: 'Hot Tub Removal' }); // cache.update -> updated job.state was set to cache
// now, if you call your cached query, you'll see that it's already been notified of the update!
const jobAfter = await getJobByUuid({ uuid: jobUuid }); // cache.hit -> query.output was fetched from cache
expect(jobAfter.name).toEqual('Hot Tub Removal')
this works due to domain.object dereferencing
under the hood, whenever a domain.logic.query.output references a domain.object
- we
.set
the state of the referenced domain.object into its own key in the cache - we
.set
only a reference pointer to the domain.object into the key for the query - on
.get
we then look up the domain.object state and replace all of the references with the original value - that way, every
.set
updates the shared state of the domain.object, which.get
then gets automatically ✨
✨ automatically invalidate cache on mutation
if your domain.logic.query depends on the identities or relationships of domain.objects, you can easily add automatic cache invalidation whenever these dependencies are impacted by a mutation
simply define what your domain.logic.query depends on when adding caching
import { refProperty } from 'with-domain-driven-caching';
// say you have this query and caching to it with the relationship dependency defined
const getJobsByProvider = withQueryCaching(
async ({ providerUuid }): Promise<Job[]> => { /* ... */ },
{
cache,
dependsOn: [
{
// this query depends on the relationships
relationship: {
// from this provider identified by uuid
from: {
dobj: ref(ServiceProvider),
uuid: ({ input }) => input[0].providerUuid,
},
// to any jobs
to: {
dobj: ref(Job),
},
// they have a relationship with via `Job.providerUuid`
via: ref(Job, 'providerUuid'),
}
}
]
})
this tells the caching mechanism that anytime a the relationship between provider:@uuid
and job:any
is changed, the query.output should be invalidated
therefore, when the mutation in the following example is called, the query.output is invalidated automatically
for example
// say you have a mutation which updates the providerUuid of a job
const setJobProvider = withMutationEffects(async ({ jobUuid, providerUuid }): Promise<Job> => { /* ... */ }, { cache });
// lets check the jobs we currently have for our provider
const providerUuid = '__provider_uuid__';
const jobsBefore = await getJobsByProvider({ providerUuid }); // cache.miss -> query.output was set to cache
expect(jobsBefore).toEqual(0); // lets say they have no jobs yet
// lets assign a job to them
await setJobProvider({ jobUuid, providerUuid }); // cache.update -> updated job.state was set to cache
// now, if you call your cached query, you'll see that it's been invalidated and will hit the underlying store again
const jobAfter = await getJobByUuid({ uuid: jobUuid }); // cache.miss -> query.output was set to cache again
expect(jobsBefore).toEqual(1); // now, it would find the new job
this works due to domain.logic.query dependency pointers
under the hood, whenever a domain.logic.query.dependsOn some domain.object properties
- when the domain.logic.query is executed, the exact properties it depends on are saved in the cache
- when a mutation is ran, the domain.objects it outputs get their properties compared against their cached values
- for each property detected as changed, a cache invalidation is triggered for each query that depends on the property
- that way, as soon as a queries dependencies have been impacted, the query will be invalidated automatically ✨