effector-storage v6
Published: 2023-05-08 12:25:35

Wow, that was a long jump! But now `effector-storage` version 6 [is landed](https://github.com/yumauri/effector-storage/releases/tag/v6.0.0).

So, whats new?

## Contracts support

This could be a minuscule change for a majority of this library users, but I consider this like a big step toward serious business. "Contract" sounds serious, doesn't it?

Like any source, which your code has no full control on, `localStorage` values should never be trusted and needs to be validated. Any script kiddie can open DevTools and change `localStorage` value, thus breaking your big-money application. In order to prevent it, data should be validated in two levels — structurally and businessly. `effector-storage` library cannot help you with your business logic, but at least, it can validate values against contracts now.

In a simple case, contract could be just a simple type guard:

```typescript
persist({
  store: $counter,
  key: 'counter',
  contract: (raw): raw is number => typeof raw === 'number',
  fail: failed
})
```

So, if `localStorage` somehow will contain a string now — `persist` will fail restoring store `$counter`, and trigger `failed` event with `operation: "validate"`.

Note though, that contracts are working after parsing (deserializing), so if `localStorage` value is not a valid JSON string at all — `persist` will fail, triggering `failed` event with `operation: "get"`.

In more complex cases, when you need to validate data against some complex structure with complex rules — `effector-storage` fully supports [Farfetched contracts](https://farfetched.pages.dev/api/primitives/contract.html), so you can use any adapter from Farfetched — [runtypes](https://farfetched.pages.dev/api/contracts/runtypes.html), [zod](https://farfetched.pages.dev/api/contracts/zod.html), [io-ts](https://farfetched.pages.dev/api/contracts/io-ts.html), [superstruct](https://farfetched.pages.dev/api/contracts/superstruct.html), [typed-contracts](https://farfetched.pages.dev/api/contracts/typed-contracts.html):

```typescript
import { Record, Literal, Number } from 'runtypes'
import { runtypeContract } from '@farfetched/runtypes'

const Asteroid = Record({
  type: Literal('asteroid'),
  mass: Number,
})

persist({
  store: $asteroid,
  key: 'asteroid',
  contract: runtypeContract(Asteroid),
  fail: failed
})
```

There are two gotchas with contracts:

1. From `effector-storage` point of view it is absolutely normal, when there is no persisted value in the storage yet. So, `undefined` value is _always valid_, even if contract does not explicitly allow it.
2. `effector-storage` does not prevent persisting invalid data to the storage, but it will validate it nonetheless, after persisting, so, if you write invalid data to the storage, `fail` will be triggered, but data will be persisted.

## Exported adapters

Sometimes it is required to persist different stores within different storages within the same module. And to avoid names clashes in version 5 you have to write something like this:

```typescript
import { persist as localPersist } from 'effector-storage/local'
import { persist as queryPersist } from 'effector-storage/query'

localPersist({ store: $session })
queryPersist({ store: $page })
```

In version 6 all adapters now are exported alongside with `persist` function, and could be used on their own. More over, all (major) adapters are exported from the root package as well, so you can use them all in once with a single import statement:

```typescript
import { persist, local, query } from 'effector-storage'

persist({ adapter: local, store: $session })
persist({ adapter: query, store: $page })
```

Strictly speaking, imported `local` and `query` in the example above are not _adapters_, but _adapter factories_. So you can call factory to create an _adapter instance_, if you need some adapter adjustments:

```typescript
persist({
  store: $date,
  adapter: local({
    serialize: (date) => String(date.getTime()),
    deserialize: (timestamp) => new Date(Number(timestamp)),
  }),
})
```

Or you can pass any adapter factory arguments right into `persist`!

```typescript
persist({
  store: $date,
  adapter: local,
  serialize: (date) => String(date.getTime()),
  deserialize: (timestamp) => new Date(Number(timestamp)),
})
```

## Adapter utilities

With exported adapters new idea has crossed my mind: what if you could write a high order function, which will accept adapter (or adapter factory), and slightly change it behaviour?

So, I made a few:

### `async` utility function

It takes any synchronous storage adapter (or factory) and makes it asynchronous:

```typescript
persist({
  adapter: async(local),
  store: $counter,
  done: doneEvent,
})
```

This could be useful, when you need to postpone store restoring a bit. If you don't use `pickup` option, `persist` begins restoring store state _right at the moment_. So, in case of synchronous `localStorage`, right after `persist` is executed — store value already be restored from the storage (of course, if it was persisted beforehand). Sometimes it can affect your business logic, for example, if you need to _react_ on store restoring, you need to take that behaviour into account, and define all connections (like `sample`s) before you call `persist`.

`async` utility function can help you with that, freeing you from that burden of keeping declarations order in mind.

### `either` utility function

It takes two adapters (or factories), and returns first of them, which is not "no-op".

What the heck is "no-op" adapter?

Version 6 introduces two separate "terms", or "states" for adapters:
- Supported — this means, that storage is supported by environment, for example, `localStorage` is supported in browsers, but not supported in Nodejs.
- Available — this means, that storage is supported and available, for example, `localStorage` could be forbidden by browser security policies.

So, if adapter is doing _nothing_ (like `nil` adapter), or it is not _supported_ — this is "no-op" adapter.

```typescript
persist({
  store: $store,
  adapter: either(local, log),
  key: 'store'
})
```

In the example above store `$store` will be persisted in `localStorage` in the browser, and will use new `log` adapter, which just prints operations, in Nodejs.

**Breaking change**❗️ In version 5 `local` adapter was behaving like _not supported_ (and did nothing) when `localStorage` is forbidden by browser security policies. In version 6 this considered like _not available_, thus, `local` adapter will trigger security error on each operation. You can handle this and ask user to enable `localStorage`, for example.

### `farcached` utility function

It takes any cache adapter from [Farfetched](https://farfetched.pages.dev/api/operators/cache.html) and converts it to be used as `persist` adapter :)

This one I made mostly out of fun and to show possibilities, which opens with utility functions approach.

From real usage point of view, using Farfetched cache adapters could be useful, when you need logic for cache invalidation, because all of its adapters have `maxAge` option out of the box.

Also, you could use Farfetched cache adapters to inject different cache adapters with `fork` using [`cache.__.$instance`](https://farfetched.pages.dev/recipes/server_cache.html#inject-adapter) internal store.

```typescript
import { localStorageCache } from '@farfetched/core'

persist({
  store: $counter3,
  adapter: farcached(localStorageCache({ maxAge: '15m' })),
})
```

## Adapter context

This is an experimental feature, and I think it will be like dark horse for a while 🤫 But I plan to show it fully in next minor releases.

In a few words, context is aimed for two main purposes:
- Asynchronous updates from storage in _scope_
- Using different environments in adapter

You can pass _context_ in two ways:
- As `pickup` event payload
- As new `context` event payload

`persist` will remember _scope_, in which you have called `pickup` or `context`, and will use this scope for any asynchronous updates from storage (like on `"storage"` event).

Also, _context_ will be passed as a second argument in adapter's `get` and `set` functions, and adapter can use it as it pleases.

## Force sync mode

Due to minor performance optimizations, when `local` adapter receives `"storage"` event — it takes new storage value from this event's payload. Obviously, there is no need to read (relatively slow) `localStorage` when new value is right here, inside the already received event.

But in some cases this approach could lead to stores desynchronization with simultaneous or almost simultaneous updates from different tabs. You can call this _race condition_.

This happens because `"storage"` event is happening _only_ when storage is modified from _another_ tab/window. So, in any situation with simultaneous or almost simultaneous `localStorage` updates from more than one tab — there always will be a loser tab, which will become unsynchronized. I've drawn an image with two tabs: each tab receives update from another tab, and one of the tabs inevitably become unsynchronized with `localStorage`:

![stores desynchronization example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3hfi4vqtr26s15c721b8.png)

I consider this very rare case, so, version 6 does not change default behaviour (with using new value from event), but adds new possible value for `sync` option instead — `'force'`:

```typescript
persist({
  store: $balance,
  key: 'balance',
  sync: 'force'
})
```

This will ignore new value from `"storage"` event, and will forcibly read new value from `localStorage` instead, thus eliminating desynchronization issue.

## Throttle/batch writes

Version 6 adds new option `timeout` for three adapters: `local`, `session` and `query`. It behaves slightly differently for them, but meaning is plain — postpone writes to a storage.

For `local` and `session` (and base `storage`) adapters `timeout` option behaves like _throttle_ for the single `persist` (without leading write). So, if you have store, which updates frequently and persisted in the `localStorage`, you can slightly improve performance by throttling access to the `localStorage` with this option.

For `query` adapter `timeout` option behaves like _throttle_ (without leading write) **and** _batch_. So, if you have many stores, which are persisted in a query string, you can use this option to throttle and batch the query string updates.

## Drop Effector v21 support

Version 5 fully supports Effector v21, which was out on July 6, 2020 — almost three years ago. This is possible, because `effector-storage` uses only base Effector's functionality, and because Effector has an excellent backward compatibility.

But times go on, and Effector v22 has been out on August 31, 2021 — almost two years ago. `effector-storage` already has some small tweaks to support different versions and new features, so, I think, it is time to stop supporting version 21.

![it's time](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n80o6s6gr83n0lfz663w.png)

Also, Nodejs v14 has reached end-of-life on April 30, 2023 — so, version 6 also drops it. And so should you ;)

That is all I got for you today, stay tuned ;)