npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

normalized-cache

v0.0.3

Published

A cache for storing normalized data.

Downloads

81

Readme

Normalized Cache

This normalized cache provides the following functionality:

  • Data (de)normalization
  • Data subscriptions
  • Data validation
  • Data invalidation
  • Data expiration
  • Computed fields
  • Optimistic updates
  • Garbage collection

The library is around 6 KB gzipped.

Setup

Installation:

npm install --save normalized-cache

Usage

import { Cache, schema } from "normalized-cache";

const Author = schema.object({
  name: "Author",
});

const Post = schema.object({
  name: "Post",
  fields: {
    author: Author,
  },
});

const cache = new Cache({
  types: [Post],
});

cache.write({
  type: "Post",
  data: {
    id: "1",
    title: "Title",
    author: {
      id: "2",
      name: "Name",
    },
  },
});

const { data } = cache.read({
  type: "Post",
  id: "1",
});

const { data } = cache.read({
  type: "Author",
  id: "2",
});

API

class Cache {
  get(entityID: string, optimistic?: boolean): Entity | undefined;
  set(entity: Entity, optimistic?: boolean): Entity;
  identify(options: IdentifyOptions): string | undefined;
  read(options: ReadOptions): ReadResult;
  write(options: WriteOptions): WriteResult;
  delete(options: DeleteOptions): DeleteResult;
  invalidate(options: InvalidateOptions): InvalidateResult;
  watch(options: WatchOptions): Unsubscribable;
  silent(fn: () => void): void;
  transaction(fn: () => void): void;
  reset(): void;
  gc(): void;
  retain(entityID: string): Disposable;
  addOptimisticUpdate(updateFn: OptimisticUpdateFn): OptimisticUpdateDisposable;
  removeOptimisticUpdate(id: number): void;
}

const schema = {
  array(config?: ArrayTypeConfig | ValueType): ArrayType
  boolean(config?: BooleanTypeConfig): BooleanType
  nonNullable(config: NonNullableTypeConfig | ValueType): NonNullableType
  number(config?: NumberTypeConfig): NumberType
  object(config?: ObjectTypeConfig): ObjectType
  string(config?: StringTypeConfig | string): StringType
  union(config: UnionTypeConfig | ValueType[]): UnionType
}

Schema

Schema types allow you to define entities, relationships and fields.

Learn more about the type system here.

Writing

When writing to the cache, a type must be provided.

cache.write({
  type: "Post",
  data: { id: "1", title: "Title" },
});

A ID can be specified if this cannot be inferred from the data itself:

cache.write({
  type: "Post",
  id: "1",
  data: { title: "Title" },
});

If the ID is an object or array it will be automatically serialized to a stable string:

cache.write({
  type: "Posts",
  id: { page: 1, limit: 10 },
  data: [],
});

Reading

Reading from the cache can be done with the read method.

Without selector

When no selector is given, all data related to the entity will be returned:

cache.write({
  type: "Author",
  data: {
    id: "2",
    name: "Author",
  },
});

cache.write({
  type: "Post",
  data: {
    id: "1",
    title: "Title",
    author: {
      id: "2",
    },
  },
});

const { data } = cache.read({
  type: "Post",
  id: "1",
});

console.log(data);

// {
//   id: "1",
//   title: "Title",
//   author: {
//     id: "2",
//     name: "Author",
//   },
// }

The resulting data can contain circular references when entities refer to each other.

With selector

Selectors can be used to select specific fields:

import { cql } from "normalized-cache";

cache.write({
  type: "Author",
  data: {
    id: "2",
    name: "Author",
  },
});

cache.write({
  type: "Post",
  data: {
    id: "1",
    title: "Title",
    author: {
      id: "2",
    },
  },
});

const { data } = cache.read({
  type: "Post",
  id: "1",
  select: cql`{ title author { name } }`,
});

console.log(data);

// {
//   title: "Title",
//   author: {
//     name: "Author",
//   },
// }

Learn more about selectors here.

With write selector

The write method also returns a selector that matches the exact shape of the input:

cache.write({
  type: "Author",
  data: {
    id: "2",
    name: "Author",
  },
});

const { selector } = cache.write({
  type: "Post",
  data: {
    id: "1",
    title: "Title",
    author: {
      id: "2",
    },
  },
});

const { data } = cache.read({
  type: "Post",
  id: "1",
  select: selector,
});

console.log(data);

// {
//   id: "1",
//   title: "Title",
//   author: {
//     id: "2",
//   },
// }

Computed fields

Computed fields can be created by defining a field with a read function.

Defining a computed field for calculations:

const Cart = schema.object({
  name: "Cart",
  fields: {
    totalPrice: {
      read: (cart) => {
        return cart.items.reduce((total, item) => total + item.price, 0);
      },
    },
  },
});

Defining a relational field based on another field:

const Author = schema.object({
  name: "Author",
});

const Post = schema.object({
  name: "Post",
  fields: {
    author: {
      read: (post, { toReference }) => {
        return toReference({ type: "Author", id: post.authorId });
      },
    },
  },
});

Invalid fields

Fields that do not match with the schema will be reported in the invalidFields array:

const LoggedIn = schema.boolean({ name: "LoggedIn" })

const cache = new Cache({ types: [LoggedIn] });

cache.write({ type: "LoggedIn" data: "string" });

const { invalidFields } = cache.read({ type: "LoggedIn" });

if (invalidFields) {
  console.log("Invalid data");
}

Missing fields

Fields that are missing will be reported in the missingFields array:

const LoggedIn = schema.boolean({ name: "LoggedIn" });

const cache = new Cache({ types: [LoggedIn] });

const { missingFields } = cache.read({ type: "LoggedIn" });

if (missingFields) {
  console.log("Missing data");
}

Stale flag

The stale flag indicates if some entity or field has been invalidated or if any expiresAt has past:

const LoggedIn = schema.boolean({ name: "LoggedIn" });

const cache = new Cache({ types: [LoggedIn] });

cache.write({ type: "LoggedIn" data: true, expiresAt: 0 });

const { stale } = cache.read({ type: "LoggedIn" });

if (stale) {
  console.log("Stale data");
}

Watching

Data in the cache can be watched with the watch method.

Watching for any change in a specific post and all related data:

const { unsubscribe } = cache.watch({
  type: "Post",
  id: "1",
  callback: (result, prevResult) => {
    // log
  },
});

unsubscribe();

Watching specific fields:

cache.watch({
  type: "Post",
  id: "1",
  select: cql`{ title }`,
  callback: (result, prevResult) => {
    if (!prevResult.stale && result.stale) {
      // The title became stale
    }
  },
});

Invalidation

Entities and fields can be invalidated with the invalidate method.

When an entity or field is invalidated, all related watchers will be notified.

Invalidate an entity:

cache.invalidate({
  type: "Post",
  id: "1",
});

Invalidate entity fields:

cache.invalidate({
  type: "Post",
  id: "1",
  select: cql`{ comments }`,
});

Expiration

when expiresAt is specified, all affected fields will be considered stale after the given time:

cache.write({
  type: "Post",
  data: { id: "1" },
  expiresAt: Date.now() + 60 * 1000,
});

Set expiration for certain types:

cache.write({
  type: "Post",
  data: { id: "1" },
  expiresAt: {
    Comment: Date.now() + 60 * 1000,
  },
});

Deletion

Entities and fields can be deleted with the delete method.

Deleting an entity:

cache.delete({
  type: "Post",
  id: "1",
});

Deleting specific fields:

cache.delete({
  type: "Post",
  id: "1",
  select: cql`{ title }`,
});

Optimistic updates

An optimistic update function can be used to update the cache optimistically.

These functions will be executed everytime the cache is updated, until they are removed.

This means that if new data is written to the cache, the optimistic update will be re-applied / rebased on top of the new data.

async function addComment(postID, text) {
  function addCommentToPost(comment) {
    const { data } = cache.read({
      type: "Post",
      id: postID,
      select: cql`{ comments }`,
    });

    cache.write({
      type: "Post",
      id: postID,
      data: { comments: [...data.comments, comment] },
    });
  }

  const { dispose } = cache.addOptimisticUpdate(() => {
    const optimisticComment = { id: uuid(), text };
    addCommentToPost(optimisticComment);
  });

  const comment = await api.addComment(postID, text);

  cache.transaction(() => {
    dispose();
    addCommentToPost(comment);
  });
}

Merging

By default entities are shallowly merged and non-entity values are replaced.

This behavior can be customized by defining custom write functions on entities and fields.

Replacing entities instead of merging:

const Author = schema.object({
  name: "Author",
  write: (incoming) => {
    return incoming;
  },
});

Merging objects instead of replacing:

const Post = schema.object({
  name: "Post",
  fields: {
    content: {
      type: schema.object(),
      write: (incoming, existing) => {
        return { ...existing, ...incoming };
      },
    },
  },
});

Transforming values when writing:

const Post = schema.object({
  name: "Post",
  fields: {
    title: {
      write: (incoming) => {
        if (typeof incoming === "string") {
          return incoming.toUpperCase();
        }
      },
    },
  },
});

Transactions

Multiple changes can be wrapped in a transaction to make sure watchers are only notified once after the last change:

cache.transaction(() => {
  cache.write({ type: "Post", data: { id: "1", title: "1" } });
  cache.write({ type: "Post", data: { id: "2", title: "2" } });
});

Silent changes

Wrap changes with silent to prevent watchers from being notified:

cache.silent(() => {
  cache.write({ type: "Post", data: { id: "1", title: "1" } });
});

Garbage collection

The gc method can be used to remove all unwatched and unreachable entities from the cache.

Use the retain method to prevent an entity from being removed.