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

@zetql/core

v0.1.2

Published

No boilerplate<br /> Light and Easy state management library. <br/> Inspired by Zustand and react-query. (and some naming from rxjs)

Downloads

1

Readme

@zetql/core (vanila)

No boilerplate Light and Easy state management library. Inspired by Zustand and react-query. (and some naming from rxjs)

Feature

  1. Simple state manager (subject)
  2. Easy cache management and Share (cache)
  3. Fetch and Refetch System for query management (query)
  4. infinite paging management (infiniteQuery)
  5. pure vanilla & no boiler plate
  6. Easy implementation

Subject

Simple State Manager

createSubject takes object or function returning object as its argument to create init data

const subject = createSubject({isOpen: true})
const subject = createSubject( (set, get, subscribe) => ({ isOpen: true}) )

getter

const state = subject() // { isOpen: true }
// or you can take projector to select the state you only want
const isOpen = subject((s) => s.isOpen )

1. static setter

import { createSubject } from '@zetql/core';

export const detailModalSubject = createSubject<{
  isOpen: boolean;
}>({ isOpen: false });

const { isOpen } = detailModalSubject();
// also you can use projector like the below.
// const isOpen = detailModalSubject((s) => s.isOpen)
const closeModal = () => detailModalSubject.setState({ isOpen: false });

const unsubscribe = detailModalSubject.subscribe(({ isOpen }) => {
  alert(isOpen ? 'modal is open' : 'modal is closed');
});

2. method setter

import { createSubject } from '@zetql/core';

export const detailModalSubject = createSubject<{
  isOpen: boolean;
  openModal: () => void;
  closeModal: () => void;
  toggleModal: () => void;
}>((set) => {
  return {
    isOpen: false,
    openModal: () => {
      set({ isOpen: true });
    },
    closeModal: () => {
      set({ isOpen: false });
    },
    toggleModal: () => {
      set((state) => {
        return { ...state, isOpen: !state.isOpen };
      });
    },
  };
});
const { isOpen, openModal, closeModal, toggleModal } = detailModalSubject();

QuerySubject

Fetch and Refetch System of query management

fetchQuery & refetchQuery

  • fetchQuery: fetchQuery if the cache is stale
  • refetchQuery: calls fetchQuery with lately executed params (regardless its result whether it succeeded or failed)
import { createQuery } from '@zetql/core';
import { CouponInterface } from '...'

interface CouponListState {
  coupons: Array<CouponInterface>;
}

export const couponQuery = createQuery<CouponListState, void>({
  query: () => {
    return fetch('/coupons')
      .then(({ data }) => {
        return { coupons: data };
      })
  },
  initData: { coupons: [] },
});

const state1 = couponQuery();
// {
//   error: null, 
//   data: { coupons: [] }, 
//   dataDeps: undefined, 
//   lastDeps: undefined, 
//   isFetching: false, 
//   isLoading: false, 
//   isRefetching: false, 
//   initiated: false
// }
couponQuery.fetchQuery();
const fetchingStatus = couponQuery();
// { ..., 
// isFetching: true, // query fetching it
// isRefetching: false, 
// ...}
...
couponQuery.refetchQuery();

const refetchingStatus = couponQuery();
// { ..., 
// isFetching: true,
// isRefetching: true, 
// ...}

QueryState

type QueryState<State, Deps = any> = {
/**
 * only when the latest one has error
 */
error: Error | null;
/**
 * data from successful query
 */
data: State;

/**
 * lastly successes query parameter
 */
dataDeps: Deps | undefined;
/**
 * lastly requested query arguments, it can be in progress
 * lastDeps === dataDeps means
 * no query is in process & last one was successful
 * lastDeps !== dataDeps means
*/
lastDeps: Deps | undefined;

/**
 * query function is being called
 */
isFetching: boolean;

/**
 * no cache found and query function is being called
 * isLoading === false && isFetching === true
 * means you have proper api for current deps to show before new query finishes
 *
 * isLoading === true && isFetching === true
 * means you do not have proper api for current deps to show before new query finishes
 */
isLoading: boolean;
  
/**
* refetching flag
* */
isRefetching: boolean;

/**
 * query function has been called
 * */
initiated: boolean;
};

subscribe To State

  • subscribe: subscribe to every change of the state
  • subscribeData: subscribe to only data
  • subscribeError: subscribe to only error
couponQuery.subscribe((state) => {
  console.log(state);
});
couponQuery.subscribeData((state) => {
  // only subscribe to data update
  console.log(state);
  // { data: {coupons: [....]},
  //   deps: undefined,
  //   queryId: '...',
  //   fromCache: false,
  // }
});
couponQuery.subscribeError((state) => {
  // only subscribe to error update
  console.log(state);
  // { error: Error; deps: undefined; queryId: '...' }
});

const { fetchQuery } = couponQuery;
const queryId = fetchQuery();
couponQuery.isActiveQuery(queryId)
// true

Query Retry Strategy

  • retryCount: query retry count when the query is failed
  • retryInterval: intervalTime between retries(ms)
export const couponQuery = createQuery<CouponListState, void>({
  query: () => {
    //....
  },
  initData: { coupons: [] },
  retryCount: 2, // another 2times, default is 0
  retryInterval: 300, // 300ms
});

Query Refetch Strategy


export const couponQuery = createQuery<CouponListState, void>({
  query: () => {
    //...
  },
  initData: { coupons: [] },
  refetchOnReconnect: true, 
  refetchOnVisibilityChange: true,
});
// refetchOnReconnect & refetchOnVisibilityChange
// check cache is stale, only when cache is stale refetch is processing


// looping refetch
const { startInterval, stopInterval } = couponQuery
startInterval(3000)
startInterval(5000) // only keep one interval
stopInterval()

Query Cache

querySubject takes cache options to create cache

  • staleTime(ms): span of time that one query data is valid
  • extraCacheTime(ms): how long cache can be kept once the data are stale

Even a cache is stale, you can use it as placeholder before new query is executed. if there is no cache from data, previous data would be displayed. If we found the cache, it turns isLoading flag on as well as isFetching

import { couponQuery } from "examples/src/subjects/coupon/coupon.query";

export const couponQuery = createQuery<CouponListState, undefined>({
  query: fetchCoupons,
  initData: { coupons: [] },
}, {
  staleTime: 10 * 60_000, // 10minutes 
  extraCacheTime: 10 * 60_000 // another 10minutes to keep it
});

/**
 * the cache is stale but extraCacheTime is still valid
 */
couponQuery.fetchQuery()
console.log(couponQuery())
// {..., isFetching: true, isLoading: false}

/**
 * both statleTime & extraCacheTime have passed
 */
couponQuery.fetchQuery()
console.log(couponQuery())
// { isFetching: true, isLoading: true }
  • isFetching: query is activated with/out stale cache available
  • isLoading: query is activated without stale cache available

Query Cache Key

cache can be grouped by cacheGroupBy it should return string. Also cacheKey can be anything and is compared by shallowEqual. For the sakes of performance, better to set cacheKeyBy.

export const couponQuery = createQuery<CouponListState, {size: number}>({
  query: fetchCoupons,
  initData: { coupons: [] },
  cacheKeyBy: (a) => a,
  cacheGroupBy: ({ size }) => size.toString()
}, {
  staleTime: 10 * 60_000, // stale cache for 10minutes
  extraCacheTime: 10 * 60_000 // 10minutes cacheing for stale cache
});

Query Cache Share

cache can be shared among querySubjects. instead of cache options, you can create cacheDB and pass it as cache option

import { createCacheDB } from '@zetql/core'

const projectCache = createCacheDB(
  {
    staleTime: 10 * 60_000,
    extraCacheTime: 10 * 60_000
  }
)
const fetchProject = ({ projectName }) => {
  //....
}

export const panelProject = createQuery<ProjectData, { projectName: string }>({
  query: fetchCoupons,
  cacheKeyBy: ({projectName}) => projectName,
  initData: { data: null },
}, projectCache );

export const mainProject = createQuery<ProjectData, { projectName: string}>({
  query: fetchCoupons,
  cacheKeyBy: ({projectName}) => projectName,
  initData: { data: null },
}, projectCache);

Infinite Query Subject

InfiniteQuery is working for infinitePagination. It takes cursor(which means param of the query and should have form of object) to call and takes getNextCursor and getPrevCursor to execute pagination. To initiate the query use fetchQuery and fetchNext to call next(fetchPrev to call prev). If getNextCursor or getPrevCursor is not provided, fetchNext and fetchPrev would not work respectfully.

import { createInfiniteQuery } from '@zetql/core';
interface StockList {
  stocks: Array<StockModel>;
  nextCursor: number | undefined;
}
const fetchStocks: (param?: {
  offset: number | undefined;
}) => Promise<StockList> = (offset) => {
  const url =`/stocks-relative-cursor${
    typeof offset?.offset === 'number' ? `?offset=${offset?.offset}` : ''
  }`
  return fetch(url).then(({ data, nextCursor }) => {
    return { stocks: data, nextCursor };
  });
};

const stockQuery = createInfiniteQuery<
  StockList,
  { offset: number | undefined }
>({
  query: fetchStocks,
  normalize: (pages) => {
    return pages.reduce((p: StockModel[], c) => {
      return p.concat(c.data.stocks);
    }, []);
  },
  cacheKeyBy(cursor){
    return cursor.offset
  },
  getNextCursor: ({ cursor, data }) => {
    return data.nextCursor ? { offset: data.nextCursor } : null;
  }
});
const { fetchNext, fetchQuery } = stockQuery;
fetchQuery({ offset: 0 });
fetchNext();

getNextCursor | getPrevQuery

CursorCreator is function to provide prev or next cursor. It gets endcursor(the last success data at the edge) and error if error occurred. For instance, getNextCursor gets the last success data of forwards direction.

type CursorCreator<QData, Cursor> =  (
  endCursor: { data: QData; cursor: Cursor },
  errorData: { error: Error; cursor: Cursor } | null
) => Cursor | null;

CursorMode

cursorMode can be static or relative.

static means parameter for the query is already decided depends on which page is requested like the typical pagination. Also you are sure that the response is pretty identical.

// this is static cursor
fetchPage({ size: 30, page: 2 })
fetchPage({ size: 30, page: 3 })
// it is always 3 after 2

relative means depends on the response the request would be different, and response can be vary time to time and it is critical.

// relative
const response = await fetchPage({ cursor: 0 });
// { data: [...], nextCursor: 10 }
fetchPage({ cursor: response.nextCursor });
// cursor can be different depend on the response

cursor mode can be critical to call refetchQuery and refetchStale. In static cursorMode, refetching query would work in parallel and regardless of status of the prev query(fail or success), it keeps refetching to the end. Also if

refetchErrorSettleMode

on error while refetching queries

  • partial : it just update partial response (works only with cursorMode: static)
  • all-or-none : it drops every response

if you'd like to use it for react and want to use hooks, use @zetql/react instead

Using PaginationGroup

Grouping is supported with name passed on fetchQuery

fetchQuery({ offset: 0 }, 'size:20');
fetchQuery({ offset: 0 }, 'size:30');
fetchQuery({ offset: 0 }, 'q=la&size=30');
fetchQuery({ offset: 0 }, 'q=la');

when you switch groups, if previous query exists, cache can be used by passing function return the cursor it gets, instead of new cursor. argument cursor can be null, should return cursor always.

/**
 * use cache on switching group
 * */
fetchQuery( (cursor) => {
    if (cursor) {
        return cursor;
    }
    return { page: 1, category: '1' };
}, 'category=1');

/**
 * do not use cache on switching group
 * */
fetchQuery({ page: 1, category: '1' }, 'category=1');

Data Normalize

data from infiniteQuery is stored as array Array<{ cursor, data }> normalize is projector to change these forms.

/**
 * without normalize
 * */
const stockQuery = createInfiniteQuery<StockModel>({
  // ...
});
stockQuery().data
// [ { stocks: ... }, ... ]

/**
 * with normalize
 * */
const noramlizedStockQuery = createInfiniteQuery({
  normalize: (pages) => {
    return pages.reduce((p: StockModel[], c: StockModel) => {
      return p.concat(c.data.stocks);
    }, []);
  },
  // ...
});
noramlizedStockQuery().data
// [...]

Infinite Query State

interface InfiniteQueryState<QData, Cursor, Normalized> {
  data: Normalized;
  isFetching: boolean;
  isFetchingNext: boolean;
  isFetchingPrev: boolean;
  isRefetching: boolean;
  groupKey: string;  // current group key
  lastCursor: Cursor | null;
  nextCursor: Cursor | null;
  prevCursor: Cursor | null;
  error: CursorErrorData<Cursor> | null;
}