Storage

Every model definition is connected to its storage. By default, the model definition uses memory attached to the persistent cache layer. You can define a custom storage by the [store.connect] property in the model definition. It can be synchronous (memory, localStorage, etc.), as well as asynchronous, like external APIs.

Memory

The [store.connect] property is not defined.

Singleton

For a singleton, the memory storage always returns an instance of the model. It initializes new instance when model is accessed for the first time. On other hand, deleting the model resets values to defaults rather than removes the model instance from the memory.

const Globals = {
  someValue: 123,
};

// logs: 123
console.log(store.get(Globals).someValue);

// logs: true
console.log(store.get(Globals) === store.get(Globals));

When the model is deleted, within the next call the default values will be returned:

store.set(Globals, null).then(() => {
  // logs: 123
  console.log(store.get(Globals).someValue);
})

Enumerable

For the enumerables, the memory storage returns only models, which were created before. It also supports listing type of the definition. However, it does not support passing string or object id. It always returns all of the instances.

const Todo = {
  id: true,
  desc: "",
  checked: false,
};

// Logs an array with all of the instances of the Todo model in memory
console.log(store.get([Todo]));

External

To define custom storage set the [store.connect] property in the model definition:

const Model = {
  ...,
  [store.connect] : {
    get?: (id) => {...},
    set?: (id, values, keys) => {...},
    list?: (id) => {...},
    cache?: boolean | number [ms] = true,
  },
};

All of the options are optional, but at least get or list method must be defined.

get

get: (id: string | object) => object | null | Promise<object | null>
  • arguments:

    • id - undefined, a string or object model instance identifier

  • returns (required):

    • null, an object representing model values or a promise resolving to the values for the model

If the storage definition only contains get method, you can use shorter syntax using a function as a value of the [store.connect] property (it works similar to the translation feature of the property descriptor):

const Model = {
  ...,
  // equals to { get: (id) => {...} }
  [store.connect] : (id) => {...},
};

set

set: (id: undefined | string | object, values: object, keys: [string]) => object | null | Promise<object | null>
  • arguments:

    • id - undefined, a string or object model instance identifier

    • values - an object with new values combined with current model state

    • keys - a list of names of the updated properties

  • returns (required):

    • null, an object representing model values or a promise resolving to the values for the model

When set method is omitted, the model definition become read-only, and it is not possible to update values. As the store.set() method supports partial values, the keys argument contains a list of actually updated properties (it might be helpful if the storage supports partial update).

The configuration does not provide a separate method for creating a new instance of the model definition. Then the id field is set to undefined. Still, the values contains the id property generated on the client-side.

{
  set(id, values) {
    if (id === undefined) {
      return api.create(values);
    }

    return api.update(id, values);
  }
}

list

list: (id: undefined | string | object) => [object] | Promise<[object]>
  • arguments:

    • id - undefined, a string or object model instance identifier

  • returns (required):

    • an array of model instances or a promise resolving to the array of models

Use list method for listing enumerable model definition. The listing type creates its own cache space mapped by the id, respecting the cache setting of the storage. It supports passing string identifier or object record.

const Movie = {
  id: true,
  title: "",
  ...,
  [store.connect] : {
    get: (id) => movieApi.get(id),
    list: ({ query, year }) => movieApi.search({ query, year }),
  },
};

const MovieList = {
  query: '',
  year: 2020,
  movies: store([Movie], (host) => ({ query: host.query, year: host.year })),
  render: ({ query, year, movies }) => html`
    <input value="${query}" oninput="${html.set("query")}" />
    <input type="number" value="${year}" oninput="${html.set("year")}" />
    <ul>
      ${store.ready(movies) && movies.map(movie => html`<li>...</li>`)}
    </ul>
  `,
}

In the above example, the list method uses the search feature of the API. Using the listing type, we can display a result page with movies filtered by query and year. However, the result of the listing mode cannot contain additional metadata. For such a case, create a separate definition with a nested array of models.

import Movie from "./movie.js";

const MovieSearchResult = {
  items: [Movie],
  offset: 0,
  limit: 0,
  [store.connect] : {
    get: ({ query, year}) => movieApi.search({ query, year }),
  };
};

cache

cache: boolean | number [ms] = `true`

The cache option sets expiration time for the cached value of the model instance. By default, it is set to true, which makes the cache persistent. Then, the data source is called only once or again if the cache is invalidated manually (explained below).

If cache is set to a number, it represents a time to invalidation counted in milliseconds. Expired models are not automatically fetched, or deleted from the cache. Only the next call for the model after its expiration time fetches data from the source again.

For the high-frequency data, set cache value to false or 0. Then, each call for the model will fetch data from the store. Usually, the store is used inside of the component properties, so its value is cached also on the host property level. Updating other properties won't trigger fetching data again (use cache invalidation for manual update).

Invalidation

Both memory and external storage uses a global cache mechanism based on the model definition reference. Model instances are global, so the cache mechanism cannot automatically predict which instance is no longer required. Because of that, the store provides store.clear() method for invalidating model instances by the model definition or specific instance of the model.

store.clear(model: object, clearValue?: boolean = true)
  • arguments:

    • model - a model definition (for all instances) or a model instance (for a specific one)

    • clearValue - indicates if the cached value should be deleted (true), or it should only notify the cache mechanism, that the value expired, but leaves the value untouched (false)

For example, it might be useful to set the clearValue to false for the case when you want to implement the refresh button. Then, the values stay in the cache, but the store will fetch the next version of the instance.

import Email from "./email.js";

function refresh(host) {
  store.clear(host.emails, false);
}

const MyElement = {
  emails: store([Email]),
  render: ({ emails }) => html`
    <button onclick="${refresh}">Refresh</button>

    ${store.ready(emails) && ...}
  `,
}

Garbage Collector

The store.clear() method works as a garbage collector for unused model instances. Those that are not a dependency of any component will be deleted entirely from the cache registry (as they would never exist) protecting from the memory leaks. It means, that even if you set clearValue to false, those instances that are not currently attached to the components, will be permanently deleted when store.clear() method is used.

Last updated