Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: persist extension #277

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

noveogroup-amorgunov
Copy link

Hey. I tried to implement persist layer #49 . This PR is based on #87

You can see example of usage in my pet project: https://github.com/noveogroup-amorgunov/nukeapp/blob/90a0f75f57f877d4889033456f3628c5d1e34699/src/shared/lib/server/serverDb.ts#L49-L51 (PR: noveogroup-amorgunov/nukeapp#14)

If this approach is OK, I am going to write tests.

@noveogroup-amorgunov noveogroup-amorgunov changed the title Feat persist extension feat: persist extension Jun 2, 2023
factory: FactoryAPI<any>,
options: ExtensionOption = {},
) {
if (!isBrowser() || (!options.storage && !supports.sessionStorage())) {
Copy link

@chazmuzz chazmuzz Jun 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great if server-side persistence could be supported too. I have that need in my project. I want to serialise my mock DB and store it in Redis. Seems doable if all persist function needs is a storage method that has getItem/setItem methods?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If @kettanaito will ok with this API, I could change interface for custom storage

@Kamahl19
Copy link

@kettanaito would you please take a look on this?

@Kamahl19 Kamahl19 mentioned this pull request Jul 12, 2023
2 tasks
@timsofteng
Copy link

Is it possible to merge it?

@Kamahl19
Copy link

Kamahl19 commented Oct 22, 2023

According to this #285 no new features will be merged. I have modified @noveogroup-amorgunov 's code to be used outside of mswjs-data internal code.

You can see it being used in this project https://github.com/Kamahl19/react-starter/blob/main/src/mocks/persist.ts . I will keep the most up-to-date version there.

Usage:

import { factory, primaryKey } from '@mswjs/data';
const db = factory({ ... });
persist(db);

Create persist.ts with this code

import debounce from 'lodash/debounce';
import {
  DATABASE_INSTANCE,
  ENTITY_TYPE,
  PRIMARY_KEY,
  type FactoryAPI,
  type Entity,
  type ModelDictionary,
  type PrimaryKeyType,
} from '@mswjs/data/lib/glossary';
import {
  type SerializedEntity,
  SERIALIZED_INTERNAL_PROPERTIES_KEY,
} from '@mswjs/data/lib/db/Database';
import { inheritInternalProperties } from '@mswjs/data/lib/utils/inheritInternalProperties';

const STORAGE_KEY_PREFIX = 'mswjs-data';

// Timout to persist state with some delay
const DEBOUNCE_PERSIST_TIME_MS = 10;

type Models<Dictionary extends ModelDictionary> = Record<
  keyof Dictionary,
  Map<PrimaryKeyType, Entity<Dictionary, any>> // eslint-disable-line @typescript-eslint/no-explicit-any
>;

type SerializedModels<Dictionary extends ModelDictionary> = Record<
  keyof Dictionary,
  Map<PrimaryKeyType, SerializedEntity>
>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function persist<Dictionary extends ModelDictionary>(
  factory: FactoryAPI<Dictionary>,
) {
  if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') {
    return;
  }

  const db = factory[DATABASE_INSTANCE];

  const key = `${STORAGE_KEY_PREFIX}/${db.id}`;

  const persistState = debounce(function persistState() {
    // eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/consistent-type-assertions
    const models = db['models'] as Models<Dictionary>;
    // eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/consistent-type-assertions
    const serializeEntity = db['serializeEntity'] as (
      entity: Entity<Dictionary, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
    ) => SerializedEntity;

    const json = Object.fromEntries(
      Object.entries(models).map(([modelName, entities]) => [
        modelName,
        Array.from(entities, ([, entity]) => serializeEntity(entity)),
      ]),
    );

    sessionStorage.setItem(key, JSON.stringify(json));
  }, DEBOUNCE_PERSIST_TIME_MS);

  function hydrateState() {
    const initialState = sessionStorage.getItem(key);

    if (initialState) {
      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
      const data = JSON.parse(initialState) as SerializedModels<Dictionary>;

      for (const [modelName, entities] of Object.entries(data)) {
        for (const entity of entities.values()) {
          db.create(modelName, deserializeEntity(entity));
        }
      }
    }

    // Add event listeners only after hydration
    db.events.on('create', persistState);
    db.events.on('update', persistState);
    db.events.on('delete', persistState);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', hydrateState);
  } else {
    hydrateState();
  }
}

function deserializeEntity(entity: SerializedEntity) {
  const { [SERIALIZED_INTERNAL_PROPERTIES_KEY]: internalProperties, ...publicProperties } = entity;

  inheritInternalProperties(publicProperties, {
    [ENTITY_TYPE]: internalProperties.entityType,
    [PRIMARY_KEY]: internalProperties.primaryKey,
  });

  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
  return publicProperties as Entity<any, any>;
}

@wfifer
Copy link

wfifer commented Oct 10, 2024

You can see it being used in this project https://github.com/Kamahl19/react-starter/blob/main/src/mocks/persist.ts . I will keep the most up-to-date version there.

@Kamahl19 thanks so much for this! I'm using @msw/data to help our sales team run more effective product demos, and your persist has unlocked a lot of helpful functionality.

One thing I've found: collocating updates when a model has a manyOf relationship is broken after the page reloads and the database is rehydrated. E.g. my user model had expenses: manyOf('expense'), and creating a new expense while updating a user's expenses would start throwing an "invariant" error only after reloading the page. I've worked around this by storing less on the user and adding user id to the various other models. It works just as well, but thought I'd call this out here in case anyone else runs into the same issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants