Security News
Weekly Downloads Now Available in npm Package Search Results
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
@data-eden/cache
Advanced tools
fetch
layer, websockets, etc).cache.$debug
that are stripped in production builds, but are lazily loadable.// Primary "user-land" API for creating a new cache instance
type DefaultRegistry = Record<string, unknown>;
export function buildCache<CacheKeyRegistry = DefaultRegistry, $Debug=unknown, UserExtensionData=unknown>(options?: CacheOptions<CacheKeyRegistry, $Debug, UserExtensionData>): Cache<CacheKeyRegistry, $Debug, UserExtensionData>;
type CacheEntry<CacheKeyRegistry, UserExtensionData=unknown> = [key: Key extends keyof CacheKeyRegistry, value: CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>];
export interface CacheEntrySerializer<CacheKeyRegistry, Key extends keyof CacheKeyRegistry, SerializedValue = unknown> {
serialize(cacheEntry: CacheEntry<CacheKeyRegistry>): [Key, SerializedValue, CacheEntryState<UserExtensionData>];
deserialize(cacheEntry: [Key, SerializedValue, CacheEntryState<UserExtensionData>]): CacheEntry<CacheKeyRegistry>;
}
export interface Cache<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> {
beginTransaction(): CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData>;
async get<Key extends keyof CacheKeyRegistry>(cacheKey: Key): CacheKeyRegistry[Key] | undefined;
/**
Generator function that yields each of the cache entries. Note that this
will include both strongly held (unexpired entries) as well as weakly held
entries.
*/
[Symbol.asyncIterator]<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>]>
/**
Generator function that yields each of the cache entries. Note that this
will include both strongly held (unexpired entries) as well as weakly held
entries.
*/
entries<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>]>
entryRevisions<Key extends keyof CacheKeyRegistry>(cacheKey: Key): AsyncIterableIterator<[entity: CacheKeyRegistry[Key], revision: number][]>;
keys<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<Key>
values<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<CacheKeyRegistry[Key]>
/**
Calling `.save()` without a serializer will iterate over the cache entries
and return an array of cache entry tuples. The values contained within the
tuples are copied via `structuredClone`.
If your cache entries are not structured clonable, (e.g. a function)
`.save()` will throw an error. In this case, use the alternate form of
`.save` passing in a `CacheEntrySerializer`.
@see <https://developer.mozilla.org/en-US/docs/Web/API/structuredClone>
*/
async save<Key extends keyof CacheKeyRegistry>(): [Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>][];
/**
Calling `.save()` passing a `CacheEntrySerializer` will iterate over the
cache entries and pass each cache entry tuple in to the `serializer.serialize()`
function. The `serializer.serialize()` method is expected to return a
serialized cache entry tuple that its `serializer.deserialize()` can
interprete back into a cache entry tuple.
This is mainly intended to allow cache values that are not structured
clonable to be saved/loaded. If all of your cached values are structured
clonable (e.g. simple POJOs, strings, numbers, booleans, etc) you do not
need to pass a `CacheEntrySerializer` and you can call `.save()` (without
arguments).
*/
async save<Key extends keyof CacheKeyRegistry>(serializer: CacheEntrySerializer): ReturnType<CacheEntrySerializer>[];
/**
Calling `.load()` will add all entries passed to the cache.
Note: `.load()` does not clear pre-existing entries, if you need to clear
entries before loading call `.clear()`.
*/
async load<Key extends keyof CacheKeyRegistry>(entries: CacheEntry<CacheKeyRegistry>[]): void;
async load<Key extends keyof CacheKeyRegistry>(serializer: CacheEntrySerializer): ReturnType<CacheEntrySerializer>[];
/**
Evict all entries from the cache.
*/
clear(): void;
readonly get options(): CacheOptions<CacheKeyRegistry, $Debug, UserExtensionData>;
$debug: $Debug & CacheDebugAPIs;
}
export interface CacheOptions<CacheKeyRegistry=DefaultRegistry,$Debug=unknown, UserExtensionData=unknown> {
hooks: {
/**
An optional callback that is invoked just before a transaction is committed.
This does not allow users to mutate the transaction, but it is a hook where
custom retention policies can be implemented.
The default retention policies are all implementable in userland as commit hooks.
*/
commit?: (tx: CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData>) => void;
/**
An optional hook for merging new versions of an entity into the cache. This
hook specifies the default behaviour for the cache -- a different merge
strategy can be passed in per call to `LiveCacheTransaction.merge`
The hook returns the updated merged entry -- it may not mutate any of its arguments.
If unspecified, the default merge strategy is to deeply merge objects.
*/
entitymergeStrategy?: EntityMergeStrategy<CacheKeyRegistry, $Debug, UserExtensionData>;
/**
An optional hook for merging the list of revisions for a cache entry.
If unspecified, the default retention strategy is to keep the full history
of an entry as long as it's in the cache, evicting revisions only when the
value itself is evicted.
*/
revisionMergeStrategy?: RevisionMergeStrategy;
}
expiration?: ExpirationPolicy;
$debug?: $Debug;
}
type ExpirationPolicy = false | {
lru: number;
ttl: number;
}
export interface EntityMergeStrategy<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> {
<Key extends keyof CacheKeyRegistry>(cacheKey: Key, newEntityRevision: CachedEntityRevision<CacheKeyRegistry[Key]>, current: CacheKeyRegistry[Key] | undefined, tx: CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData>): CacheKeyValue;
}
export interface RevisionMergeStrategy<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> {
<Key extends keyof CacheKeyRegistry>(cacheKey: Key, tx: CommittingTransaction<CacheKeyRegistry, $Debug, UserExtensionData>): void;
}
export interface CacheEntryState<UserExtenionData=unknown> {
retained: {
lru: boolean;
ttl: number;
};
/**
The last time this cache entry was accessed, either via `get`, `set`, or
`merge`.
Mainly useful for userland retention policies.
*/
lastAccessed: number;
extensions: UserExtensionData;
}
export interface CacheDebugAPIs {
size(): void;
entries(): void;
history(): void;
}
export interface CacheTransactionDebugAPIs {
size(): void;
entries(): void;
}
interface CachedEntityRevision<CacheKeyValue> {
entity: CacheKeyValue;
revision: number;
}
export interface CacheTransaction<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> {
/**
Get the value of `cacheKey` in the cache. If `key` has been modified in this
transaction (e.g. via `merge` or `set`), `tx.get` will return the updated
entry in this transaction. The return value can therefore differ from
`cache.get`.
*/
async get<Key extends keyof CacheKeyRegistry>(cacheKey: Key): CacheKeyRegistry[Key] | undefined;
localEntries<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState]>
entries<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState]>
/**
Generator function that yields each of the transaction local entries.
*/
[Symbol.asyncIterator]<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>]>
/**
An async generator that produces the revisions of `key` within this transaction.
*/
localRevisions<Key extends keyof CacheKeyRegistry>(cacheKey: Key): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry[Key]>>;
/**
An async generator that produces the complete list of revisions for `key`,
from the time the transaction began and including the revisions added in this
transaction.
*/
entryRevisions<Key extends keyof CacheKeyRegistry>(cacheKey: Key): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry[Key]>>;
$debug: $Debug & CacheTransactionDebugAPIs;
}
export interface CommittingTransaction<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> extends CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData> {
cache: {
clearRevisions<Key extends keyof CacheKeyRegistry>(id: Key): void;
appendRevisions<Key extends keyof CacheKeyRegistry>(id: Key, revisions: CachedEntityRevision<CacheKeyRegistry[Key]>[]): void;
}
}
export interface LiveCacheTransaction<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> extends CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData> {
async merge<Key extends keyof CacheKeyRegistry>(cacheKey: Key, value: CachedEntityRevision<CacheKeyRegistry[Key]>, options?: {
entityMergeStrategy: EntityMergeStrategy<CacheKeyRegistry, $Debug, UserExtensionData>;
revisionMergeStrategy: RevisionMergeStrategy<CacheKeyRegistry, $Debug, UserExtensionData>;
$debug: $Debug;
}): Promise<CacheKeyRegistry[Key]>;
async set<Key extends keyof CacheKeyRegistry>(cacheKey: Key, value: CacheKeyRegistry[Key]): Promise<CacheKeyRegistry[Key]>;
async delete<Key extends keyof CacheKeyRegistry>(cacheKey: Key): Promise<boolean>;
/**
Updates the cache values.
* Entities with an updated value (either from merging or setting) are
updated in the cache (i.e. calling cache.get(key) will return the values
set in the transation)
* Entities deleted from the transaction are deleted from the cache.
Updates the cache revisions.
* The retention merge strategy is called for each entity with new
revisions. It is called with a CommittingTransaction and has the
opportunity to update the saved revisions for an entity.
**Transaction Conflicts**
During a transaction, merges occurred between new revisions of an entity and
the revision in the cache at the time the transaction began. If a separate
transaction has committed since then, these merges may be out of date. When
this happens, `mergeStrategy` will be called again during `commit` to resolve
these merge conflicts.
**Transaction Timeout**
`commit` is atomic -- when it is called it will attempt to acquire the write
lock for the cache. After `timeout` milliseconds the promise will reject if
it has not yet acquired the lock.
If `timeout` is `false` then commit will immediately rejectd if it cannot
acquire the write lock.
*/
async commit(options?: {
timeout: number | false = false;
}): Promise<void>;
/**
Abandon this transaction and discard any changes or other transaction state.
*/
rollback(): void;
}
export function defaultMergeStrategy(): MergeStrategy;
// TODO: re-write as a tutorial + example // TODO: explain how to do type-aware cache registry; see https://tsplay.dev/NrnDlN
// query 1
let query1 = {
data: {
bookstore: {
id: 'urn:bookstore:1',
books: [
{
metadata: 'urn:author:1',
// soldInBookstores: ['urn:li:bookstore:1']
soldInBookstores: [
{
id: 'urn:bookstore:1',
city: 'London',
},
],
// author: 'urn:author:1',
author: {
id: 'urn:author:1',
name: 'JK Rowling',
},
},
],
topSellingAuthor: {
id: 'urn:author:1',
name: 'JK Rowling',
},
},
},
};
// query 2
let query2 = {
data: {
author: {
id: 'urn:author:1',
name: 'Winston Churchill',
},
},
};
let doc1 = executeQuery(query1);
let bookAuthor = doc1.data.bookstore.books[0].author;
let topSellingAuthor = doc1.data.bookstore.topSellingAuthor;
expect(bookAuthor).toBe(topSellingAuthor);
expect(bookAuthor.name).toEqual('JK Rowling');
let doc2 = executeQuery(query2);
let author = doc2.data.author;
expect(author).toBe(bookAuthor);
expect(bookAuthor.name).toEqual('Winston Churchill');
expect(doc2.data.author).toBe(doc1.data.topSellingAuthor);
let cache = buildCache();
type CachedEntities = any;
let DocumentEntityMap = new WeakMap();
let DocumentProxyMap = new WeakMap();
let globalRevisionCounter = 0;
async function handleGraphQLResponse(
requestUrl: string,
responseBody: object,
parseEntities: (document) => TimestampedEntity[]
entityId: (entity: object) => string,
queryMetaData: QueryMetaData,
options: OperationOptions,
) {
let documentKey = requestUrl;
let document = responseBody;
let rawDocument;
/*
Trampoline debugging -- the then block here is transformed into a no-op
function that is replaced by the real function lazily when debugging is
loaded.
*/
if(hasDebug()) {
// Exact implementation TBD
// maybe Response.clone().json()
// maybe JSON.parse(JSON.stringify)
// maybe rawDocument = awit response.text(); document = JSON.parse(rawDocument);
rawDocument = deepCopy(document);
}
let defaultRevision = ++globalRevisionCounter;
let tx = cache.beginTransaction();
let txSucceeded = false;
try {
let cachedEntities = [];
// parseEntities yields in post-order depth-first-search i.e. children before parents
for(let { entity, parent, prop, revision: entityRevision } of await parseEntities(document, queryMetaData)) {
let id = entityId(entity);
let revision = entityRevision ?? defaultRevision;
let { entityMergeStrategy, revisionMergeStrategy } = options;
// merges with pre-existing entities (along with debugging data) and
// returns the merged result
let mergedEntity = await tx.merge(id, entity, { revision, entityMergeStrategy, revisionMergeStrategy, $debug: { rawDocument } });
// For example, parent === book, prop === 'author'
// Because all userland calls go through GraphQL operations, we have
// the metadata necessary to differentiate strings from references
parent[prop] = id;
cachedEntities.push(mergedEntity);
}
DocumentEntityMap.set(document, cachedEntities);
// Proxy exists to do at least the following:
// 1. access referenced cached entities and
// 2. field mask them
let documentProxy = new DocumentProxy(document, queryMetaData, { $debug: { rawDocument } });
DocumentProxyMap.set(document, documentProxy);
// override any previous item for this documentKey
await tx.set(documentKey, document);
await tx.commit();
txSucceeded = true;
return documentProxy;
} finally {
if(!txSucceeded) {
tx.rollback();
}
}
}
const defaultMergeStrategy = deepMergeStrategy;
async function shallowMergeStrategy<CacheKeyRegistry>(id, { entity, revision }, current: CacheKeyValue | undefined, tx: CacheTransaction<CacheKeyRegistry>) {
return Object.assign({}, current, entity);
}
async function deepMergeStrategy<CacheKeyRegistry>(id, { entity, revision }, current: CacheKeyValue | undefined, tx: CacheTransaction<CacheKeyRegistry>) {
// ...
}
async function lastWriteWinsStrategy<CacheKeyRegistry>(id, { entity, revision }, current: CacheKeyValue | undefined, tx: CacheTransaction<CacheKeyRegistry>) {
return entity;
}
async function lastWriteWinsTimestampStrategy<CacheKeyRegistry>(id, { entity, revision }, current?: CacheKeyValue, tx: CacheTransaction<CacheKeyRegistry>) {
let revisionsGenerator = tx.entryRevisions(id);
let priorRevision = await revisionsGenerator.next()?.value;
if(priorRevision) {
return revision > priorRevision.revision ? entity : priorRevision.entity;
}
return entity;
}
// how a user might merge differently by type
async function forkedMergeStrategy<CacheKeyRegistry>(id, newEntityRevision, current?: CacheKeyValue, tx: CacheTransaction<CacheKeyRegistry>) {
let type = userLandParseType(id);
switch(type) {
case 'SomeType':
// ...
return deepMergeStrategy(id, newEntityRevision, current, cache);
default:
// ...
return shallowMergeStrategy(id, newEntityRevision, current, cache);
}
}
// example cache with custom merge
let cacheWithCustomMerge = buildCache({ hooks: { entityMergeStrategy: lastWriteWinsTimestampStrategy }});
async function take(generator, n) {
if(n <= 0) {
return [];
}
let i = 0;
let result = [];
for (let value of await generator) {
result.push(value);
if (++i == n) {
return result;
}
}
return result;
}
const defaultRetensionStrategy = retainAllRevisions;
async function retainNoRevisions(id, tx: CommittingTransaction<CacheKeyRegistry>) {
}
async function retainAllRevisions(id, tx: CommittingTransaction<CacheKeyRegistry>) {
tx.cache.appendRevisions([...await tx.localRevisions(id)]);
}
async function retainLast5Revisions(id, tx: CommittingTransaction<CacheKeyRegistry>) {
let lastFiveRevisions = await take(tx.entryRevisions(id), 5);
tx.cache.clearRevisions(id);
tx.cache.appendRevisions(lastFiveRevisions);
}
let cacheWithCustomRevisionRetention = buildCache({ hooks: { revisionMergeStrategy: retainLast5Revisions }});
// userland ttl
let ttlRetention = new Map(); // Map not WeakMap so entries retained
let cacheWithUserlandTTL = buildCache({ hooks: { commit: userTTL }});
const ttl = 5_000; // 5 seconds
async function userTTL(tx) {
for (let [key, value] of tx.entries()) {
ttlRetention.set(key, value);
setTimeout(ttl, function() {
ttlRetention.delete(key);
});
}
}
FAQs
## Principles
We found that @data-eden/cache demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
Security News
A Stanford study reveals 9.5% of engineers contribute almost nothing, costing tech $90B annually, with remote work fueling the rise of "ghost engineers."
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.