vue-composable
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -1,10 +0,14 @@ | ||
export * from './event'; | ||
export * from './arrayPagination'; | ||
export * from './debounce'; | ||
export * from './onMouseMove'; | ||
export * from './onResize'; | ||
export * from './onScroll'; | ||
export * from './pagination'; | ||
export * from './promise'; | ||
export * from './cancellablePromise'; | ||
export * from './fetch'; | ||
export * from "./event/event"; | ||
export * from "./pagination/arrayPagination"; | ||
export * from "./debounce"; | ||
export * from "./event/onMouseMove"; | ||
export * from "./event/onResize"; | ||
export * from "./event/onScroll"; | ||
export * from "./pagination/pagination"; | ||
export * from "./promise/promise"; | ||
export * from "./promise/cancellablePromise"; | ||
export * from "./promise/retry"; | ||
export * from "./web/fetch"; | ||
export * from "./web/axios"; | ||
export * from "./web/webSocket"; | ||
export * from "./localStorage"; |
import { Ref } from "@vue/composition-api"; | ||
export declare type RefTyped<T> = T | Ref<T>; | ||
export declare type RefElement = RefTyped<Element>; | ||
export declare function unwrap<T>(o: RefTyped<T>): T; | ||
export declare function wrap<T>(o: RefTyped<T>): Ref<T>; | ||
export declare function promisedTimeout(timeout: number): Promise<unknown>; | ||
export declare const isArray: (arg: any) => arg is any[]; | ||
export declare const isFunction: (val: unknown) => val is Function; | ||
export declare const isDate: (val: unknown) => val is Date; | ||
export declare const isNumber: (val: unknown) => val is number; | ||
export declare const isObject: (val: unknown) => val is Record<any, any>; | ||
export declare function isPromise<T = any>(val: unknown): val is Promise<T>; | ||
export declare function promisedTimeout(timeout: number): Promise<void>; | ||
export declare function minMax(val: number, min: number, max: number): number; |
@@ -5,18 +5,29 @@ 'use strict'; | ||
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } | ||
var compositionApi = require('@vue/composition-api'); | ||
var axios = _interopDefault(require('axios')); | ||
function useEvent(el, name, listener, options) { | ||
const element = compositionApi.isRef(el) ? el.value : el; | ||
const remove = () => element.removeEventListener(name, listener); | ||
compositionApi.onMounted(() => element.addEventListener(name, listener, options)); | ||
compositionApi.onUnmounted(remove); | ||
return remove; | ||
} | ||
function unwrap(o) { | ||
return compositionApi.isRef(o) ? o.value : o; | ||
} | ||
// export function unwrap<T>(o: RefTyped<T>): T { | ||
// return isRef(o) ? o.value : o; | ||
// } | ||
function wrap(o) { | ||
return compositionApi.isRef(o) ? o : compositionApi.ref(o); | ||
} | ||
const isFunction = (val) => typeof val === "function"; | ||
// export const isString = (val: unknown): val is string => | ||
// typeof val === "string"; | ||
// export const isSymbol = (val: unknown): val is symbol => | ||
// typeof val === "symbol"; | ||
const isDate = (val) => isObject(val) && isFunction(val.getTime); | ||
const isNumber = (val) => typeof val === "number"; | ||
const isObject = (val) => val !== null && typeof val === "object"; | ||
function isPromise(val) { | ||
return isObject(val) && isFunction(val.then) && isFunction(val.catch); | ||
} | ||
function promisedTimeout(timeout) { | ||
return new Promise(res => { | ||
setTimeout(res, timeout); | ||
}); | ||
} | ||
function minMax(val, min, max) { | ||
@@ -30,2 +41,10 @@ if (val < min) | ||
function useEvent(el, name, listener, options) { | ||
const element = wrap(el); | ||
const remove = () => element.value.removeEventListener(name, listener); | ||
compositionApi.onMounted(() => element.value.addEventListener(name, listener, options)); | ||
compositionApi.onUnmounted(remove); | ||
return remove; | ||
} | ||
function usePagination(options) { | ||
@@ -161,4 +180,3 @@ const _currentPage = wrap(options.currentPage); | ||
function useMouseMove(el, options, wait) { | ||
const element = unwrap(el); | ||
function useOnMouseMove(el, options, wait) { | ||
const mouseX = compositionApi.ref(0); | ||
@@ -175,3 +193,3 @@ const mouseY = compositionApi.ref(0); | ||
} | ||
const remove = useEvent(element, "mousemove", handler, eventOptions); | ||
const remove = useEvent(el, "mousemove", handler, eventOptions); | ||
return { | ||
@@ -185,8 +203,9 @@ mouseX, | ||
function useOnResize(el, options, wait) { | ||
const element = unwrap(el); | ||
const height = compositionApi.ref(element.clientHeight); | ||
const width = compositionApi.ref(element.clientWidth); | ||
let handler = (ev) => { | ||
height.value = element.clientHeight; | ||
width.value = element.clientWidth; | ||
const element = wrap(el); | ||
const height = compositionApi.ref(element.value && element.value.clientHeight); | ||
const width = compositionApi.ref(element.value && element.value.clientWidth); | ||
let handler = () => { | ||
debugger; | ||
height.value = element.value.clientHeight; | ||
width.value = element.value.clientWidth; | ||
}; | ||
@@ -207,8 +226,8 @@ const eventOptions = typeof options === "number" ? undefined : options; | ||
function useOnScroll(el, options, wait) { | ||
const element = unwrap(el); | ||
const scrollTop = compositionApi.ref(element.scrollTop); | ||
const scrollLeft = compositionApi.ref(element.scrollLeft); | ||
const element = wrap(el); | ||
const scrollTop = compositionApi.ref(element.value && element.value.scrollTop); | ||
const scrollLeft = compositionApi.ref(element.value && element.value.scrollLeft); | ||
let handler = (ev) => { | ||
scrollTop.value = element.scrollTop; | ||
scrollLeft.value = element.scrollLeft; | ||
scrollTop.value = element.value.scrollTop; | ||
scrollLeft.value = element.value.scrollLeft; | ||
}; | ||
@@ -239,3 +258,2 @@ const eventOptions = typeof options === "number" ? undefined : options; | ||
const promise = compositionApi.ref(); | ||
let lastPromise = null; | ||
const exec = async (...args) => { | ||
@@ -245,6 +263,6 @@ loading.value = true; | ||
result.value = null; | ||
const currentPromise = (promise.value = lastPromise = fn(...args)); | ||
const currentPromise = (promise.value = fn(...args)); | ||
try { | ||
const r = await currentPromise; | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
result.value = r; | ||
@@ -255,3 +273,3 @@ } | ||
catch (er) { | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
error.value = er; | ||
@@ -263,3 +281,3 @@ result.value = null; | ||
finally { | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
loading.value = false; | ||
@@ -297,9 +315,151 @@ } | ||
function useFetch(init) { | ||
const MAX_RETRIES = 9000; | ||
/* istanbul ignore next */ | ||
const ExecutionId = Symbol(process.env.NODE_ENV !== "production" ? "RetryId" : undefined); | ||
/* istanbul ignore next */ | ||
const CancellationToken = Symbol(process.env.NODE_ENV !== "production" ? "CancellationToken" : undefined); | ||
const defaultStrategy = async (options, context, factory, args) => { | ||
const retryId = context[ExecutionId].value; | ||
let i = -1; | ||
const maxRetries = options.maxRetries || MAX_RETRIES + 1; | ||
const delay = options.retryDelay || noDelay; | ||
context.retryErrors.value = []; | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
let nextRetry = undefined; | ||
do { | ||
let success = false; | ||
let result = null; | ||
try { | ||
++i; | ||
if (args) { | ||
result = factory(...args); | ||
} | ||
else { | ||
result = factory(); | ||
} | ||
if (isPromise(result)) { | ||
result = await result; | ||
} | ||
// is cancelled? | ||
if (context[CancellationToken].value) { | ||
return null; | ||
} | ||
success = true; | ||
} | ||
catch (error) { | ||
result = null; | ||
context.retryErrors.value.push(error); | ||
} | ||
// is our retry current one? | ||
if (retryId !== context[ExecutionId].value) { | ||
return result; | ||
} | ||
if (success) { | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
return result; | ||
} | ||
if (i >= maxRetries) { | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
return Promise.reject(new Error(`[useRetry] max retries reached ${maxRetries}`)); | ||
} | ||
context.isRetrying.value = true; | ||
const now = Date.now(); | ||
const pDelayBy = delay(i); // wrapped | ||
const delayBy = isPromise(pDelayBy) ? await pDelayBy : pDelayBy; // unwrap promise | ||
if (!isPromise(pDelayBy) || !!delayBy) { | ||
if (isNumber(delayBy)) { | ||
nextRetry = delayBy; | ||
} | ||
else if (isDate(delayBy)) { | ||
nextRetry = delayBy.getTime(); | ||
} | ||
else { | ||
throw new Error(`[useRetry] invalid value received from options.retryDelay '${typeof delayBy}'`); | ||
} | ||
// if the retry is in the past, means we need to await that amount | ||
if (nextRetry < now) { | ||
context.nextRetry.value = now + nextRetry; | ||
} | ||
else { | ||
context.nextRetry.value = nextRetry; | ||
nextRetry = nextRetry - now; | ||
} | ||
if (nextRetry > 0) { | ||
await promisedTimeout(nextRetry); | ||
} | ||
} | ||
// is cancelled? | ||
if (context[CancellationToken].value) { | ||
return null; | ||
} | ||
// is our retry current one? | ||
if (retryId !== context[ExecutionId].value) { | ||
return result; | ||
} | ||
} while (i < MAX_RETRIES); | ||
return null; | ||
}; | ||
function useRetry(options, factory) { | ||
const opt = !options || isFunction(options) ? {} : options; | ||
const fn = isFunction(options) ? options : factory; | ||
if (!isFunction(options) && !isObject(options)) { | ||
throw new Error("[useRetry] options needs to be 'object'"); | ||
} | ||
if (!!fn && !isFunction(fn)) { | ||
throw new Error("[useRetry] factory needs to be 'function'"); | ||
} | ||
const isRetrying = compositionApi.ref(false); | ||
const nextRetry = compositionApi.ref(); | ||
const retryErrors = compositionApi.ref([]); | ||
const cancellationToken = { value: false }; | ||
const retryId = { value: 0 }; | ||
const retryCount = compositionApi.computed(() => retryErrors.value.length); | ||
const context = { | ||
isRetrying, | ||
retryCount, | ||
nextRetry, | ||
retryErrors, | ||
[ExecutionId]: retryId, | ||
[CancellationToken]: cancellationToken | ||
}; | ||
const exec = fn | ||
? (...args) => { | ||
++context[ExecutionId].value; | ||
return defaultStrategy(opt, context, fn, args); | ||
} | ||
: (f) => { | ||
++context[ExecutionId].value; | ||
return defaultStrategy(opt, context, f, undefined); | ||
}; | ||
const cancel = () => { | ||
context.isRetrying.value = false; | ||
context.retryErrors.value.push(new Error("[useRetry] cancelled")); | ||
context.nextRetry.value = undefined; | ||
cancellationToken.value = true; | ||
}; | ||
return { | ||
...context, | ||
cancel, | ||
exec | ||
}; | ||
} | ||
const exponentialDelay = retryNumber => { | ||
const delay = Math.pow(2, retryNumber) * 100; | ||
const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay | ||
return delay + randomSum; | ||
}; | ||
const noDelay = () => 0; | ||
function useFetch(options) { | ||
const json = compositionApi.ref(null); | ||
// TODO add text = ref<string> ?? | ||
const jsonError = compositionApi.ref(null); | ||
const use = usePromise(async (request) => { | ||
const isJson = options ? options.isJson !== false : true; | ||
const parseImmediate = options ? options.parseImmediate !== false : true; | ||
const use = usePromise(async (request, init) => { | ||
const response = await fetch(request, init); | ||
if (!init || init.isJson !== false) { | ||
if (isJson) { | ||
const pJson = response | ||
@@ -312,3 +472,3 @@ .json() | ||
}); | ||
if (!init || init.parseImmediate !== false) { | ||
if (parseImmediate) { | ||
await pJson; | ||
@@ -330,4 +490,154 @@ } | ||
/* istanbul ignore next */ | ||
const _axios = axios || (globalThis && globalThis.axios); | ||
function useAxios(config) { | ||
/* istanbul ignore next */ | ||
process.env.NODE_ENV !== "production" && !_axios && console.warn(`[axios] not installed, please install it`); | ||
const axiosClient = _axios.create(config); | ||
const client = compositionApi.computed(() => axiosClient); | ||
const use = usePromise(async (request) => { | ||
return axiosClient.request(request); | ||
}); | ||
const data = compositionApi.computed(() => (use.result.value && use.result.value.data) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.data) || | ||
null); | ||
const status = compositionApi.computed(() => (use.result.value && use.result.value.status) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.status) || | ||
null); | ||
const statusText = compositionApi.computed(() => (use.result.value && use.result.value.statusText) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.statusText) || | ||
null); | ||
return { | ||
...use, | ||
client, | ||
data, | ||
status, | ||
statusText | ||
}; | ||
} | ||
function useWebSocket(url, protocols) { | ||
const ws = new WebSocket(url, protocols); | ||
const messageEvent = compositionApi.ref(null); | ||
const errorEvent = compositionApi.ref(); | ||
const data = compositionApi.ref(null); | ||
const isOpen = compositionApi.ref(false); | ||
const isClosed = compositionApi.ref(false); | ||
const errored = compositionApi.ref(false); | ||
/* istanbul ignore next */ | ||
let lastMessage = (process.env.NODE_ENV !== "production" && Date.now()) || undefined; | ||
ws.addEventListener("message", x => { | ||
messageEvent.value = x; | ||
data.value = x.data; | ||
// if the messages are to quick, we need to warn | ||
/* istanbul ignore else */ | ||
if (process.env.NODE_ENV !== "production") { | ||
if (Date.now() - lastMessage < 2) { | ||
console.warn('[useWebSocket] message rate is too high, if you are using "data" or "messageEvent"' + | ||
" you might not get updated of all the messages." + | ||
' Use "ws..addEventListener("message", handler)" instead'); | ||
} | ||
lastMessage = Date.now(); | ||
} | ||
}); | ||
ws.addEventListener("error", error => { | ||
errorEvent.value = error; | ||
errored.value = true; | ||
}); | ||
ws.addEventListener("close", () => { | ||
isOpen.value = false; | ||
isClosed.value = true; | ||
}); | ||
ws.addEventListener("open", () => { | ||
isOpen.value = true; | ||
isClosed.value = false; | ||
}); | ||
const send = (data) => ws.send(data); | ||
const close = (code, reason) => { | ||
ws.close(code, reason); | ||
}; | ||
return { | ||
ws, | ||
send, | ||
close, | ||
messageEvent, | ||
errorEvent, | ||
data, | ||
isOpen, | ||
isClosed, | ||
errored | ||
}; | ||
} | ||
// used to store all the instances of weakMap | ||
const keyedMap = new Map(); | ||
const weakMap = new WeakMap(); | ||
function useLocalStorage(key, defaultValue) { | ||
let lazy = false; | ||
let k = keyedMap.get(key); | ||
const json = localStorage.getItem(key); | ||
const storage = (k && weakMap.get(k)) || | ||
(!!defaultValue && wrap(defaultValue)) || | ||
compositionApi.ref(null); | ||
if (json && !k) { | ||
try { | ||
storage.value = JSON.parse(json); | ||
lazy = false; | ||
} | ||
catch (e) { | ||
/* istanbul ignore next */ | ||
console.warn("[useLocalStorage] error parsing value from localStorage", key, e); | ||
} | ||
} | ||
// do not watch if we already created the instance | ||
if (!k) { | ||
k = {}; | ||
keyedMap.set(key, k); | ||
weakMap.set(k, storage); | ||
compositionApi.watch(storage, storage => { | ||
if (storage === undefined) { | ||
localStorage.removeItem(key); | ||
return; | ||
} | ||
// do not overflow localStorage with updates nor keep doing stringify | ||
debounce(() => localStorage.setItem(key, JSON.stringify(storage)), 100)(); | ||
}, { | ||
deep: true, | ||
lazy | ||
}); | ||
} | ||
const clear = () => { | ||
keyedMap.forEach((v) => { | ||
const obj = weakMap.get(v); | ||
/* istanbul ignore else */ | ||
if (obj) { | ||
obj.value = undefined; | ||
} | ||
weakMap.delete(v); | ||
}); | ||
keyedMap.clear(); | ||
}; | ||
const remove = () => { | ||
keyedMap.delete(key); | ||
weakMap.delete(k); | ||
storage.value = undefined; | ||
}; | ||
return { | ||
storage, | ||
clear, | ||
remove | ||
}; | ||
} | ||
exports.debounce = debounce; | ||
exports.exponentialDelay = exponentialDelay; | ||
exports.noDelay = noDelay; | ||
exports.useArrayPagination = useArrayPagination; | ||
exports.useAxios = useAxios; | ||
exports.useCancellablePromise = useCancellablePromise; | ||
@@ -337,3 +647,4 @@ exports.useDebounce = useDebounce; | ||
exports.useFetch = useFetch; | ||
exports.useMouseMove = useMouseMove; | ||
exports.useLocalStorage = useLocalStorage; | ||
exports.useOnMouseMove = useOnMouseMove; | ||
exports.useOnResize = useOnResize; | ||
@@ -343,1 +654,3 @@ exports.useOnScroll = useOnScroll; | ||
exports.usePromise = usePromise; | ||
exports.useRetry = useRetry; | ||
exports.useWebSocket = useWebSocket; |
@@ -1,17 +0,26 @@ | ||
import { isRef, onMounted, onUnmounted, ref, computed, watch } from '@vue/composition-api'; | ||
import { isRef, ref, onMounted, onUnmounted, computed, watch } from '@vue/composition-api'; | ||
import axios from 'axios'; | ||
function useEvent(el, name, listener, options) { | ||
const element = isRef(el) ? el.value : el; | ||
const remove = () => element.removeEventListener(name, listener); | ||
onMounted(() => element.addEventListener(name, listener, options)); | ||
onUnmounted(remove); | ||
return remove; | ||
} | ||
function unwrap(o) { | ||
return isRef(o) ? o.value : o; | ||
} | ||
// export function unwrap<T>(o: RefTyped<T>): T { | ||
// return isRef(o) ? o.value : o; | ||
// } | ||
function wrap(o) { | ||
return isRef(o) ? o : ref(o); | ||
} | ||
const isFunction = (val) => typeof val === "function"; | ||
// export const isString = (val: unknown): val is string => | ||
// typeof val === "string"; | ||
// export const isSymbol = (val: unknown): val is symbol => | ||
// typeof val === "symbol"; | ||
const isDate = (val) => isObject(val) && isFunction(val.getTime); | ||
const isNumber = (val) => typeof val === "number"; | ||
const isObject = (val) => val !== null && typeof val === "object"; | ||
function isPromise(val) { | ||
return isObject(val) && isFunction(val.then) && isFunction(val.catch); | ||
} | ||
function promisedTimeout(timeout) { | ||
return new Promise(res => { | ||
setTimeout(res, timeout); | ||
}); | ||
} | ||
function minMax(val, min, max) { | ||
@@ -25,2 +34,10 @@ if (val < min) | ||
function useEvent(el, name, listener, options) { | ||
const element = wrap(el); | ||
const remove = () => element.value.removeEventListener(name, listener); | ||
onMounted(() => element.value.addEventListener(name, listener, options)); | ||
onUnmounted(remove); | ||
return remove; | ||
} | ||
function usePagination(options) { | ||
@@ -156,4 +173,3 @@ const _currentPage = wrap(options.currentPage); | ||
function useMouseMove(el, options, wait) { | ||
const element = unwrap(el); | ||
function useOnMouseMove(el, options, wait) { | ||
const mouseX = ref(0); | ||
@@ -170,3 +186,3 @@ const mouseY = ref(0); | ||
} | ||
const remove = useEvent(element, "mousemove", handler, eventOptions); | ||
const remove = useEvent(el, "mousemove", handler, eventOptions); | ||
return { | ||
@@ -180,8 +196,9 @@ mouseX, | ||
function useOnResize(el, options, wait) { | ||
const element = unwrap(el); | ||
const height = ref(element.clientHeight); | ||
const width = ref(element.clientWidth); | ||
let handler = (ev) => { | ||
height.value = element.clientHeight; | ||
width.value = element.clientWidth; | ||
const element = wrap(el); | ||
const height = ref(element.value && element.value.clientHeight); | ||
const width = ref(element.value && element.value.clientWidth); | ||
let handler = () => { | ||
debugger; | ||
height.value = element.value.clientHeight; | ||
width.value = element.value.clientWidth; | ||
}; | ||
@@ -202,8 +219,8 @@ const eventOptions = typeof options === "number" ? undefined : options; | ||
function useOnScroll(el, options, wait) { | ||
const element = unwrap(el); | ||
const scrollTop = ref(element.scrollTop); | ||
const scrollLeft = ref(element.scrollLeft); | ||
const element = wrap(el); | ||
const scrollTop = ref(element.value && element.value.scrollTop); | ||
const scrollLeft = ref(element.value && element.value.scrollLeft); | ||
let handler = (ev) => { | ||
scrollTop.value = element.scrollTop; | ||
scrollLeft.value = element.scrollLeft; | ||
scrollTop.value = element.value.scrollTop; | ||
scrollLeft.value = element.value.scrollLeft; | ||
}; | ||
@@ -234,3 +251,2 @@ const eventOptions = typeof options === "number" ? undefined : options; | ||
const promise = ref(); | ||
let lastPromise = null; | ||
const exec = async (...args) => { | ||
@@ -240,6 +256,6 @@ loading.value = true; | ||
result.value = null; | ||
const currentPromise = (promise.value = lastPromise = fn(...args)); | ||
const currentPromise = (promise.value = fn(...args)); | ||
try { | ||
const r = await currentPromise; | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
result.value = r; | ||
@@ -250,3 +266,3 @@ } | ||
catch (er) { | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
error.value = er; | ||
@@ -258,3 +274,3 @@ result.value = null; | ||
finally { | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
loading.value = false; | ||
@@ -292,9 +308,151 @@ } | ||
function useFetch(init) { | ||
const MAX_RETRIES = 9000; | ||
/* istanbul ignore next */ | ||
const ExecutionId = Symbol(process.env.NODE_ENV !== "production" ? "RetryId" : undefined); | ||
/* istanbul ignore next */ | ||
const CancellationToken = Symbol(process.env.NODE_ENV !== "production" ? "CancellationToken" : undefined); | ||
const defaultStrategy = async (options, context, factory, args) => { | ||
const retryId = context[ExecutionId].value; | ||
let i = -1; | ||
const maxRetries = options.maxRetries || MAX_RETRIES + 1; | ||
const delay = options.retryDelay || noDelay; | ||
context.retryErrors.value = []; | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
let nextRetry = undefined; | ||
do { | ||
let success = false; | ||
let result = null; | ||
try { | ||
++i; | ||
if (args) { | ||
result = factory(...args); | ||
} | ||
else { | ||
result = factory(); | ||
} | ||
if (isPromise(result)) { | ||
result = await result; | ||
} | ||
// is cancelled? | ||
if (context[CancellationToken].value) { | ||
return null; | ||
} | ||
success = true; | ||
} | ||
catch (error) { | ||
result = null; | ||
context.retryErrors.value.push(error); | ||
} | ||
// is our retry current one? | ||
if (retryId !== context[ExecutionId].value) { | ||
return result; | ||
} | ||
if (success) { | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
return result; | ||
} | ||
if (i >= maxRetries) { | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
return Promise.reject(new Error(`[useRetry] max retries reached ${maxRetries}`)); | ||
} | ||
context.isRetrying.value = true; | ||
const now = Date.now(); | ||
const pDelayBy = delay(i); // wrapped | ||
const delayBy = isPromise(pDelayBy) ? await pDelayBy : pDelayBy; // unwrap promise | ||
if (!isPromise(pDelayBy) || !!delayBy) { | ||
if (isNumber(delayBy)) { | ||
nextRetry = delayBy; | ||
} | ||
else if (isDate(delayBy)) { | ||
nextRetry = delayBy.getTime(); | ||
} | ||
else { | ||
throw new Error(`[useRetry] invalid value received from options.retryDelay '${typeof delayBy}'`); | ||
} | ||
// if the retry is in the past, means we need to await that amount | ||
if (nextRetry < now) { | ||
context.nextRetry.value = now + nextRetry; | ||
} | ||
else { | ||
context.nextRetry.value = nextRetry; | ||
nextRetry = nextRetry - now; | ||
} | ||
if (nextRetry > 0) { | ||
await promisedTimeout(nextRetry); | ||
} | ||
} | ||
// is cancelled? | ||
if (context[CancellationToken].value) { | ||
return null; | ||
} | ||
// is our retry current one? | ||
if (retryId !== context[ExecutionId].value) { | ||
return result; | ||
} | ||
} while (i < MAX_RETRIES); | ||
return null; | ||
}; | ||
function useRetry(options, factory) { | ||
const opt = !options || isFunction(options) ? {} : options; | ||
const fn = isFunction(options) ? options : factory; | ||
if (!isFunction(options) && !isObject(options)) { | ||
throw new Error("[useRetry] options needs to be 'object'"); | ||
} | ||
if (!!fn && !isFunction(fn)) { | ||
throw new Error("[useRetry] factory needs to be 'function'"); | ||
} | ||
const isRetrying = ref(false); | ||
const nextRetry = ref(); | ||
const retryErrors = ref([]); | ||
const cancellationToken = { value: false }; | ||
const retryId = { value: 0 }; | ||
const retryCount = computed(() => retryErrors.value.length); | ||
const context = { | ||
isRetrying, | ||
retryCount, | ||
nextRetry, | ||
retryErrors, | ||
[ExecutionId]: retryId, | ||
[CancellationToken]: cancellationToken | ||
}; | ||
const exec = fn | ||
? (...args) => { | ||
++context[ExecutionId].value; | ||
return defaultStrategy(opt, context, fn, args); | ||
} | ||
: (f) => { | ||
++context[ExecutionId].value; | ||
return defaultStrategy(opt, context, f, undefined); | ||
}; | ||
const cancel = () => { | ||
context.isRetrying.value = false; | ||
context.retryErrors.value.push(new Error("[useRetry] cancelled")); | ||
context.nextRetry.value = undefined; | ||
cancellationToken.value = true; | ||
}; | ||
return { | ||
...context, | ||
cancel, | ||
exec | ||
}; | ||
} | ||
const exponentialDelay = retryNumber => { | ||
const delay = Math.pow(2, retryNumber) * 100; | ||
const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay | ||
return delay + randomSum; | ||
}; | ||
const noDelay = () => 0; | ||
function useFetch(options) { | ||
const json = ref(null); | ||
// TODO add text = ref<string> ?? | ||
const jsonError = ref(null); | ||
const use = usePromise(async (request) => { | ||
const isJson = options ? options.isJson !== false : true; | ||
const parseImmediate = options ? options.parseImmediate !== false : true; | ||
const use = usePromise(async (request, init) => { | ||
const response = await fetch(request, init); | ||
if (!init || init.isJson !== false) { | ||
if (isJson) { | ||
const pJson = response | ||
@@ -307,3 +465,3 @@ .json() | ||
}); | ||
if (!init || init.parseImmediate !== false) { | ||
if (parseImmediate) { | ||
await pJson; | ||
@@ -325,2 +483,149 @@ } | ||
export { debounce, useArrayPagination, useCancellablePromise, useDebounce, useEvent, useFetch, useMouseMove, useOnResize, useOnScroll, usePagination, usePromise }; | ||
/* istanbul ignore next */ | ||
const _axios = axios || (globalThis && globalThis.axios); | ||
function useAxios(config) { | ||
/* istanbul ignore next */ | ||
process.env.NODE_ENV !== "production" && !_axios && console.warn(`[axios] not installed, please install it`); | ||
const axiosClient = _axios.create(config); | ||
const client = computed(() => axiosClient); | ||
const use = usePromise(async (request) => { | ||
return axiosClient.request(request); | ||
}); | ||
const data = computed(() => (use.result.value && use.result.value.data) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.data) || | ||
null); | ||
const status = computed(() => (use.result.value && use.result.value.status) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.status) || | ||
null); | ||
const statusText = computed(() => (use.result.value && use.result.value.statusText) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.statusText) || | ||
null); | ||
return { | ||
...use, | ||
client, | ||
data, | ||
status, | ||
statusText | ||
}; | ||
} | ||
function useWebSocket(url, protocols) { | ||
const ws = new WebSocket(url, protocols); | ||
const messageEvent = ref(null); | ||
const errorEvent = ref(); | ||
const data = ref(null); | ||
const isOpen = ref(false); | ||
const isClosed = ref(false); | ||
const errored = ref(false); | ||
/* istanbul ignore next */ | ||
let lastMessage = (process.env.NODE_ENV !== "production" && Date.now()) || undefined; | ||
ws.addEventListener("message", x => { | ||
messageEvent.value = x; | ||
data.value = x.data; | ||
// if the messages are to quick, we need to warn | ||
/* istanbul ignore else */ | ||
if (process.env.NODE_ENV !== "production") { | ||
if (Date.now() - lastMessage < 2) { | ||
console.warn('[useWebSocket] message rate is too high, if you are using "data" or "messageEvent"' + | ||
" you might not get updated of all the messages." + | ||
' Use "ws..addEventListener("message", handler)" instead'); | ||
} | ||
lastMessage = Date.now(); | ||
} | ||
}); | ||
ws.addEventListener("error", error => { | ||
errorEvent.value = error; | ||
errored.value = true; | ||
}); | ||
ws.addEventListener("close", () => { | ||
isOpen.value = false; | ||
isClosed.value = true; | ||
}); | ||
ws.addEventListener("open", () => { | ||
isOpen.value = true; | ||
isClosed.value = false; | ||
}); | ||
const send = (data) => ws.send(data); | ||
const close = (code, reason) => { | ||
ws.close(code, reason); | ||
}; | ||
return { | ||
ws, | ||
send, | ||
close, | ||
messageEvent, | ||
errorEvent, | ||
data, | ||
isOpen, | ||
isClosed, | ||
errored | ||
}; | ||
} | ||
// used to store all the instances of weakMap | ||
const keyedMap = new Map(); | ||
const weakMap = new WeakMap(); | ||
function useLocalStorage(key, defaultValue) { | ||
let lazy = false; | ||
let k = keyedMap.get(key); | ||
const json = localStorage.getItem(key); | ||
const storage = (k && weakMap.get(k)) || | ||
(!!defaultValue && wrap(defaultValue)) || | ||
ref(null); | ||
if (json && !k) { | ||
try { | ||
storage.value = JSON.parse(json); | ||
lazy = false; | ||
} | ||
catch (e) { | ||
/* istanbul ignore next */ | ||
console.warn("[useLocalStorage] error parsing value from localStorage", key, e); | ||
} | ||
} | ||
// do not watch if we already created the instance | ||
if (!k) { | ||
k = {}; | ||
keyedMap.set(key, k); | ||
weakMap.set(k, storage); | ||
watch(storage, storage => { | ||
if (storage === undefined) { | ||
localStorage.removeItem(key); | ||
return; | ||
} | ||
// do not overflow localStorage with updates nor keep doing stringify | ||
debounce(() => localStorage.setItem(key, JSON.stringify(storage)), 100)(); | ||
}, { | ||
deep: true, | ||
lazy | ||
}); | ||
} | ||
const clear = () => { | ||
keyedMap.forEach((v) => { | ||
const obj = weakMap.get(v); | ||
/* istanbul ignore else */ | ||
if (obj) { | ||
obj.value = undefined; | ||
} | ||
weakMap.delete(v); | ||
}); | ||
keyedMap.clear(); | ||
}; | ||
const remove = () => { | ||
keyedMap.delete(key); | ||
weakMap.delete(k); | ||
storage.value = undefined; | ||
}; | ||
return { | ||
storage, | ||
clear, | ||
remove | ||
}; | ||
} | ||
export { debounce, exponentialDelay, noDelay, useArrayPagination, useAxios, useCancellablePromise, useDebounce, useEvent, useFetch, useLocalStorage, useOnMouseMove, useOnResize, useOnScroll, usePagination, usePromise, useRetry, useWebSocket }; |
(function (global, factory) { | ||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@vue/composition-api')) : | ||
typeof define === 'function' && define.amd ? define(['exports', '@vue/composition-api'], factory) : | ||
(global = global || self, factory(global.vueComposable = {}, global.vueCompositionApi)); | ||
}(this, (function (exports, compositionApi) { 'use strict'; | ||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@vue/composition-api'), require('axios')) : | ||
typeof define === 'function' && define.amd ? define(['exports', '@vue/composition-api', 'axios'], factory) : | ||
(global = global || self, factory(global.vueComposable = {}, global.vueCompositionApi, global.axios)); | ||
}(this, (function (exports, compositionApi, axios) { 'use strict'; | ||
function useEvent(el, name, listener, options) { | ||
const element = compositionApi.isRef(el) ? el.value : el; | ||
const remove = () => element.removeEventListener(name, listener); | ||
compositionApi.onMounted(() => element.addEventListener(name, listener, options)); | ||
compositionApi.onUnmounted(remove); | ||
return remove; | ||
} | ||
axios = axios && axios.hasOwnProperty('default') ? axios['default'] : axios; | ||
function unwrap(o) { | ||
return compositionApi.isRef(o) ? o.value : o; | ||
} | ||
// export function unwrap<T>(o: RefTyped<T>): T { | ||
// return isRef(o) ? o.value : o; | ||
// } | ||
function wrap(o) { | ||
return compositionApi.isRef(o) ? o : compositionApi.ref(o); | ||
} | ||
const isFunction = (val) => typeof val === "function"; | ||
// export const isString = (val: unknown): val is string => | ||
// typeof val === "string"; | ||
// export const isSymbol = (val: unknown): val is symbol => | ||
// typeof val === "symbol"; | ||
const isDate = (val) => isObject(val) && isFunction(val.getTime); | ||
const isNumber = (val) => typeof val === "number"; | ||
const isObject = (val) => val !== null && typeof val === "object"; | ||
function isPromise(val) { | ||
return isObject(val) && isFunction(val.then) && isFunction(val.catch); | ||
} | ||
function promisedTimeout(timeout) { | ||
return new Promise(res => { | ||
setTimeout(res, timeout); | ||
}); | ||
} | ||
function minMax(val, min, max) { | ||
@@ -29,2 +39,10 @@ if (val < min) | ||
function useEvent(el, name, listener, options) { | ||
const element = wrap(el); | ||
const remove = () => element.value.removeEventListener(name, listener); | ||
compositionApi.onMounted(() => element.value.addEventListener(name, listener, options)); | ||
compositionApi.onUnmounted(remove); | ||
return remove; | ||
} | ||
function usePagination(options) { | ||
@@ -160,4 +178,3 @@ const _currentPage = wrap(options.currentPage); | ||
function useMouseMove(el, options, wait) { | ||
const element = unwrap(el); | ||
function useOnMouseMove(el, options, wait) { | ||
const mouseX = compositionApi.ref(0); | ||
@@ -174,3 +191,3 @@ const mouseY = compositionApi.ref(0); | ||
} | ||
const remove = useEvent(element, "mousemove", handler, eventOptions); | ||
const remove = useEvent(el, "mousemove", handler, eventOptions); | ||
return { | ||
@@ -184,8 +201,9 @@ mouseX, | ||
function useOnResize(el, options, wait) { | ||
const element = unwrap(el); | ||
const height = compositionApi.ref(element.clientHeight); | ||
const width = compositionApi.ref(element.clientWidth); | ||
let handler = (ev) => { | ||
height.value = element.clientHeight; | ||
width.value = element.clientWidth; | ||
const element = wrap(el); | ||
const height = compositionApi.ref(element.value && element.value.clientHeight); | ||
const width = compositionApi.ref(element.value && element.value.clientWidth); | ||
let handler = () => { | ||
debugger; | ||
height.value = element.value.clientHeight; | ||
width.value = element.value.clientWidth; | ||
}; | ||
@@ -206,8 +224,8 @@ const eventOptions = typeof options === "number" ? undefined : options; | ||
function useOnScroll(el, options, wait) { | ||
const element = unwrap(el); | ||
const scrollTop = compositionApi.ref(element.scrollTop); | ||
const scrollLeft = compositionApi.ref(element.scrollLeft); | ||
const element = wrap(el); | ||
const scrollTop = compositionApi.ref(element.value && element.value.scrollTop); | ||
const scrollLeft = compositionApi.ref(element.value && element.value.scrollLeft); | ||
let handler = (ev) => { | ||
scrollTop.value = element.scrollTop; | ||
scrollLeft.value = element.scrollLeft; | ||
scrollTop.value = element.value.scrollTop; | ||
scrollLeft.value = element.value.scrollLeft; | ||
}; | ||
@@ -238,3 +256,2 @@ const eventOptions = typeof options === "number" ? undefined : options; | ||
const promise = compositionApi.ref(); | ||
let lastPromise = null; | ||
const exec = async (...args) => { | ||
@@ -244,6 +261,6 @@ loading.value = true; | ||
result.value = null; | ||
const currentPromise = (promise.value = lastPromise = fn(...args)); | ||
const currentPromise = (promise.value = fn(...args)); | ||
try { | ||
const r = await currentPromise; | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
result.value = r; | ||
@@ -254,3 +271,3 @@ } | ||
catch (er) { | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
error.value = er; | ||
@@ -262,3 +279,3 @@ result.value = null; | ||
finally { | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
loading.value = false; | ||
@@ -296,9 +313,151 @@ } | ||
function useFetch(init) { | ||
const MAX_RETRIES = 9000; | ||
/* istanbul ignore next */ | ||
const ExecutionId = Symbol( "RetryId" ); | ||
/* istanbul ignore next */ | ||
const CancellationToken = Symbol( "CancellationToken" ); | ||
const defaultStrategy = async (options, context, factory, args) => { | ||
const retryId = context[ExecutionId].value; | ||
let i = -1; | ||
const maxRetries = options.maxRetries || MAX_RETRIES + 1; | ||
const delay = options.retryDelay || noDelay; | ||
context.retryErrors.value = []; | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
let nextRetry = undefined; | ||
do { | ||
let success = false; | ||
let result = null; | ||
try { | ||
++i; | ||
if (args) { | ||
result = factory(...args); | ||
} | ||
else { | ||
result = factory(); | ||
} | ||
if (isPromise(result)) { | ||
result = await result; | ||
} | ||
// is cancelled? | ||
if (context[CancellationToken].value) { | ||
return null; | ||
} | ||
success = true; | ||
} | ||
catch (error) { | ||
result = null; | ||
context.retryErrors.value.push(error); | ||
} | ||
// is our retry current one? | ||
if (retryId !== context[ExecutionId].value) { | ||
return result; | ||
} | ||
if (success) { | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
return result; | ||
} | ||
if (i >= maxRetries) { | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
return Promise.reject(new Error(`[useRetry] max retries reached ${maxRetries}`)); | ||
} | ||
context.isRetrying.value = true; | ||
const now = Date.now(); | ||
const pDelayBy = delay(i); // wrapped | ||
const delayBy = isPromise(pDelayBy) ? await pDelayBy : pDelayBy; // unwrap promise | ||
if (!isPromise(pDelayBy) || !!delayBy) { | ||
if (isNumber(delayBy)) { | ||
nextRetry = delayBy; | ||
} | ||
else if (isDate(delayBy)) { | ||
nextRetry = delayBy.getTime(); | ||
} | ||
else { | ||
throw new Error(`[useRetry] invalid value received from options.retryDelay '${typeof delayBy}'`); | ||
} | ||
// if the retry is in the past, means we need to await that amount | ||
if (nextRetry < now) { | ||
context.nextRetry.value = now + nextRetry; | ||
} | ||
else { | ||
context.nextRetry.value = nextRetry; | ||
nextRetry = nextRetry - now; | ||
} | ||
if (nextRetry > 0) { | ||
await promisedTimeout(nextRetry); | ||
} | ||
} | ||
// is cancelled? | ||
if (context[CancellationToken].value) { | ||
return null; | ||
} | ||
// is our retry current one? | ||
if (retryId !== context[ExecutionId].value) { | ||
return result; | ||
} | ||
} while (i < MAX_RETRIES); | ||
return null; | ||
}; | ||
function useRetry(options, factory) { | ||
const opt = !options || isFunction(options) ? {} : options; | ||
const fn = isFunction(options) ? options : factory; | ||
if (!isFunction(options) && !isObject(options)) { | ||
throw new Error("[useRetry] options needs to be 'object'"); | ||
} | ||
if (!!fn && !isFunction(fn)) { | ||
throw new Error("[useRetry] factory needs to be 'function'"); | ||
} | ||
const isRetrying = compositionApi.ref(false); | ||
const nextRetry = compositionApi.ref(); | ||
const retryErrors = compositionApi.ref([]); | ||
const cancellationToken = { value: false }; | ||
const retryId = { value: 0 }; | ||
const retryCount = compositionApi.computed(() => retryErrors.value.length); | ||
const context = { | ||
isRetrying, | ||
retryCount, | ||
nextRetry, | ||
retryErrors, | ||
[ExecutionId]: retryId, | ||
[CancellationToken]: cancellationToken | ||
}; | ||
const exec = fn | ||
? (...args) => { | ||
++context[ExecutionId].value; | ||
return defaultStrategy(opt, context, fn, args); | ||
} | ||
: (f) => { | ||
++context[ExecutionId].value; | ||
return defaultStrategy(opt, context, f, undefined); | ||
}; | ||
const cancel = () => { | ||
context.isRetrying.value = false; | ||
context.retryErrors.value.push(new Error("[useRetry] cancelled")); | ||
context.nextRetry.value = undefined; | ||
cancellationToken.value = true; | ||
}; | ||
return { | ||
...context, | ||
cancel, | ||
exec | ||
}; | ||
} | ||
const exponentialDelay = retryNumber => { | ||
const delay = Math.pow(2, retryNumber) * 100; | ||
const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay | ||
return delay + randomSum; | ||
}; | ||
const noDelay = () => 0; | ||
function useFetch(options) { | ||
const json = compositionApi.ref(null); | ||
// TODO add text = ref<string> ?? | ||
const jsonError = compositionApi.ref(null); | ||
const use = usePromise(async (request) => { | ||
const isJson = options ? options.isJson !== false : true; | ||
const parseImmediate = options ? options.parseImmediate !== false : true; | ||
const use = usePromise(async (request, init) => { | ||
const response = await fetch(request, init); | ||
if (!init || init.isJson !== false) { | ||
if (isJson) { | ||
const pJson = response | ||
@@ -311,3 +470,3 @@ .json() | ||
}); | ||
if (!init || init.parseImmediate !== false) { | ||
if (parseImmediate) { | ||
await pJson; | ||
@@ -329,4 +488,154 @@ } | ||
/* istanbul ignore next */ | ||
const _axios = axios || (globalThis && globalThis.axios); | ||
function useAxios(config) { | ||
/* istanbul ignore next */ | ||
!_axios && console.warn(`[axios] not installed, please install it`); | ||
const axiosClient = _axios.create(config); | ||
const client = compositionApi.computed(() => axiosClient); | ||
const use = usePromise(async (request) => { | ||
return axiosClient.request(request); | ||
}); | ||
const data = compositionApi.computed(() => (use.result.value && use.result.value.data) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.data) || | ||
null); | ||
const status = compositionApi.computed(() => (use.result.value && use.result.value.status) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.status) || | ||
null); | ||
const statusText = compositionApi.computed(() => (use.result.value && use.result.value.statusText) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.statusText) || | ||
null); | ||
return { | ||
...use, | ||
client, | ||
data, | ||
status, | ||
statusText | ||
}; | ||
} | ||
function useWebSocket(url, protocols) { | ||
const ws = new WebSocket(url, protocols); | ||
const messageEvent = compositionApi.ref(null); | ||
const errorEvent = compositionApi.ref(); | ||
const data = compositionApi.ref(null); | ||
const isOpen = compositionApi.ref(false); | ||
const isClosed = compositionApi.ref(false); | ||
const errored = compositionApi.ref(false); | ||
/* istanbul ignore next */ | ||
let lastMessage = ( Date.now()) || undefined; | ||
ws.addEventListener("message", x => { | ||
messageEvent.value = x; | ||
data.value = x.data; | ||
// if the messages are to quick, we need to warn | ||
/* istanbul ignore else */ | ||
{ | ||
if (Date.now() - lastMessage < 2) { | ||
console.warn('[useWebSocket] message rate is too high, if you are using "data" or "messageEvent"' + | ||
" you might not get updated of all the messages." + | ||
' Use "ws..addEventListener("message", handler)" instead'); | ||
} | ||
lastMessage = Date.now(); | ||
} | ||
}); | ||
ws.addEventListener("error", error => { | ||
errorEvent.value = error; | ||
errored.value = true; | ||
}); | ||
ws.addEventListener("close", () => { | ||
isOpen.value = false; | ||
isClosed.value = true; | ||
}); | ||
ws.addEventListener("open", () => { | ||
isOpen.value = true; | ||
isClosed.value = false; | ||
}); | ||
const send = (data) => ws.send(data); | ||
const close = (code, reason) => { | ||
ws.close(code, reason); | ||
}; | ||
return { | ||
ws, | ||
send, | ||
close, | ||
messageEvent, | ||
errorEvent, | ||
data, | ||
isOpen, | ||
isClosed, | ||
errored | ||
}; | ||
} | ||
// used to store all the instances of weakMap | ||
const keyedMap = new Map(); | ||
const weakMap = new WeakMap(); | ||
function useLocalStorage(key, defaultValue) { | ||
let lazy = false; | ||
let k = keyedMap.get(key); | ||
const json = localStorage.getItem(key); | ||
const storage = (k && weakMap.get(k)) || | ||
(!!defaultValue && wrap(defaultValue)) || | ||
compositionApi.ref(null); | ||
if (json && !k) { | ||
try { | ||
storage.value = JSON.parse(json); | ||
lazy = false; | ||
} | ||
catch (e) { | ||
/* istanbul ignore next */ | ||
console.warn("[useLocalStorage] error parsing value from localStorage", key, e); | ||
} | ||
} | ||
// do not watch if we already created the instance | ||
if (!k) { | ||
k = {}; | ||
keyedMap.set(key, k); | ||
weakMap.set(k, storage); | ||
compositionApi.watch(storage, storage => { | ||
if (storage === undefined) { | ||
localStorage.removeItem(key); | ||
return; | ||
} | ||
// do not overflow localStorage with updates nor keep doing stringify | ||
debounce(() => localStorage.setItem(key, JSON.stringify(storage)), 100)(); | ||
}, { | ||
deep: true, | ||
lazy | ||
}); | ||
} | ||
const clear = () => { | ||
keyedMap.forEach((v) => { | ||
const obj = weakMap.get(v); | ||
/* istanbul ignore else */ | ||
if (obj) { | ||
obj.value = undefined; | ||
} | ||
weakMap.delete(v); | ||
}); | ||
keyedMap.clear(); | ||
}; | ||
const remove = () => { | ||
keyedMap.delete(key); | ||
weakMap.delete(k); | ||
storage.value = undefined; | ||
}; | ||
return { | ||
storage, | ||
clear, | ||
remove | ||
}; | ||
} | ||
exports.debounce = debounce; | ||
exports.exponentialDelay = exponentialDelay; | ||
exports.noDelay = noDelay; | ||
exports.useArrayPagination = useArrayPagination; | ||
exports.useAxios = useAxios; | ||
exports.useCancellablePromise = useCancellablePromise; | ||
@@ -336,3 +645,4 @@ exports.useDebounce = useDebounce; | ||
exports.useFetch = useFetch; | ||
exports.useMouseMove = useMouseMove; | ||
exports.useLocalStorage = useLocalStorage; | ||
exports.useOnMouseMove = useOnMouseMove; | ||
exports.useOnResize = useOnResize; | ||
@@ -342,2 +652,4 @@ exports.useOnScroll = useOnScroll; | ||
exports.usePromise = usePromise; | ||
exports.useRetry = useRetry; | ||
exports.useWebSocket = useWebSocket; | ||
@@ -344,0 +656,0 @@ Object.defineProperty(exports, '__esModule', { value: true }); |
(function (global, factory) { | ||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@vue/composition-api')) : | ||
typeof define === 'function' && define.amd ? define(['exports', '@vue/composition-api'], factory) : | ||
(global = global || self, factory(global.vueComposable = {}, global.vueCompositionApi)); | ||
}(this, (function (exports, compositionApi) { 'use strict'; | ||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@vue/composition-api'), require('axios')) : | ||
typeof define === 'function' && define.amd ? define(['exports', '@vue/composition-api', 'axios'], factory) : | ||
(global = global || self, factory(global.vueComposable = {}, global.vueCompositionApi, global.axios)); | ||
}(this, (function (exports, compositionApi, axios) { 'use strict'; | ||
function useEvent(el, name, listener, options) { | ||
const element = compositionApi.isRef(el) ? el.value : el; | ||
const remove = () => element.removeEventListener(name, listener); | ||
compositionApi.onMounted(() => element.addEventListener(name, listener, options)); | ||
compositionApi.onUnmounted(remove); | ||
return remove; | ||
} | ||
axios = axios && axios.hasOwnProperty('default') ? axios['default'] : axios; | ||
function unwrap(o) { | ||
return compositionApi.isRef(o) ? o.value : o; | ||
} | ||
// export function unwrap<T>(o: RefTyped<T>): T { | ||
// return isRef(o) ? o.value : o; | ||
// } | ||
function wrap(o) { | ||
return compositionApi.isRef(o) ? o : compositionApi.ref(o); | ||
} | ||
const isFunction = (val) => typeof val === "function"; | ||
// export const isString = (val: unknown): val is string => | ||
// typeof val === "string"; | ||
// export const isSymbol = (val: unknown): val is symbol => | ||
// typeof val === "symbol"; | ||
const isDate = (val) => isObject(val) && isFunction(val.getTime); | ||
const isNumber = (val) => typeof val === "number"; | ||
const isObject = (val) => val !== null && typeof val === "object"; | ||
function isPromise(val) { | ||
return isObject(val) && isFunction(val.then) && isFunction(val.catch); | ||
} | ||
function promisedTimeout(timeout) { | ||
return new Promise(res => { | ||
setTimeout(res, timeout); | ||
}); | ||
} | ||
function minMax(val, min, max) { | ||
@@ -29,2 +39,10 @@ if (val < min) | ||
function useEvent(el, name, listener, options) { | ||
const element = wrap(el); | ||
const remove = () => element.value.removeEventListener(name, listener); | ||
compositionApi.onMounted(() => element.value.addEventListener(name, listener, options)); | ||
compositionApi.onUnmounted(remove); | ||
return remove; | ||
} | ||
function usePagination(options) { | ||
@@ -148,4 +166,3 @@ const _currentPage = wrap(options.currentPage); | ||
function useMouseMove(el, options, wait) { | ||
const element = unwrap(el); | ||
function useOnMouseMove(el, options, wait) { | ||
const mouseX = compositionApi.ref(0); | ||
@@ -162,3 +179,3 @@ const mouseY = compositionApi.ref(0); | ||
} | ||
const remove = useEvent(element, "mousemove", handler, eventOptions); | ||
const remove = useEvent(el, "mousemove", handler, eventOptions); | ||
return { | ||
@@ -172,8 +189,9 @@ mouseX, | ||
function useOnResize(el, options, wait) { | ||
const element = unwrap(el); | ||
const height = compositionApi.ref(element.clientHeight); | ||
const width = compositionApi.ref(element.clientWidth); | ||
let handler = (ev) => { | ||
height.value = element.clientHeight; | ||
width.value = element.clientWidth; | ||
const element = wrap(el); | ||
const height = compositionApi.ref(element.value && element.value.clientHeight); | ||
const width = compositionApi.ref(element.value && element.value.clientWidth); | ||
let handler = () => { | ||
debugger; | ||
height.value = element.value.clientHeight; | ||
width.value = element.value.clientWidth; | ||
}; | ||
@@ -194,8 +212,8 @@ const eventOptions = typeof options === "number" ? undefined : options; | ||
function useOnScroll(el, options, wait) { | ||
const element = unwrap(el); | ||
const scrollTop = compositionApi.ref(element.scrollTop); | ||
const scrollLeft = compositionApi.ref(element.scrollLeft); | ||
const element = wrap(el); | ||
const scrollTop = compositionApi.ref(element.value && element.value.scrollTop); | ||
const scrollLeft = compositionApi.ref(element.value && element.value.scrollLeft); | ||
let handler = (ev) => { | ||
scrollTop.value = element.scrollTop; | ||
scrollLeft.value = element.scrollLeft; | ||
scrollTop.value = element.value.scrollTop; | ||
scrollLeft.value = element.value.scrollLeft; | ||
}; | ||
@@ -226,3 +244,2 @@ const eventOptions = typeof options === "number" ? undefined : options; | ||
const promise = compositionApi.ref(); | ||
let lastPromise = null; | ||
const exec = async (...args) => { | ||
@@ -232,6 +249,6 @@ loading.value = true; | ||
result.value = null; | ||
const currentPromise = (promise.value = lastPromise = fn(...args)); | ||
const currentPromise = (promise.value = fn(...args)); | ||
try { | ||
const r = await currentPromise; | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
result.value = r; | ||
@@ -242,3 +259,3 @@ } | ||
catch (er) { | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
error.value = er; | ||
@@ -250,3 +267,3 @@ result.value = null; | ||
finally { | ||
if (lastPromise === currentPromise) { | ||
if (promise.value === currentPromise) { | ||
loading.value = false; | ||
@@ -284,9 +301,151 @@ } | ||
function useFetch(init) { | ||
const MAX_RETRIES = 9000; | ||
/* istanbul ignore next */ | ||
const ExecutionId = Symbol( undefined); | ||
/* istanbul ignore next */ | ||
const CancellationToken = Symbol( undefined); | ||
const defaultStrategy = async (options, context, factory, args) => { | ||
const retryId = context[ExecutionId].value; | ||
let i = -1; | ||
const maxRetries = options.maxRetries || MAX_RETRIES + 1; | ||
const delay = options.retryDelay || noDelay; | ||
context.retryErrors.value = []; | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
let nextRetry = undefined; | ||
do { | ||
let success = false; | ||
let result = null; | ||
try { | ||
++i; | ||
if (args) { | ||
result = factory(...args); | ||
} | ||
else { | ||
result = factory(); | ||
} | ||
if (isPromise(result)) { | ||
result = await result; | ||
} | ||
// is cancelled? | ||
if (context[CancellationToken].value) { | ||
return null; | ||
} | ||
success = true; | ||
} | ||
catch (error) { | ||
result = null; | ||
context.retryErrors.value.push(error); | ||
} | ||
// is our retry current one? | ||
if (retryId !== context[ExecutionId].value) { | ||
return result; | ||
} | ||
if (success) { | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
return result; | ||
} | ||
if (i >= maxRetries) { | ||
context.isRetrying.value = false; | ||
context.nextRetry.value = undefined; | ||
return Promise.reject(new Error(`[useRetry] max retries reached ${maxRetries}`)); | ||
} | ||
context.isRetrying.value = true; | ||
const now = Date.now(); | ||
const pDelayBy = delay(i); // wrapped | ||
const delayBy = isPromise(pDelayBy) ? await pDelayBy : pDelayBy; // unwrap promise | ||
if (!isPromise(pDelayBy) || !!delayBy) { | ||
if (isNumber(delayBy)) { | ||
nextRetry = delayBy; | ||
} | ||
else if (isDate(delayBy)) { | ||
nextRetry = delayBy.getTime(); | ||
} | ||
else { | ||
throw new Error(`[useRetry] invalid value received from options.retryDelay '${typeof delayBy}'`); | ||
} | ||
// if the retry is in the past, means we need to await that amount | ||
if (nextRetry < now) { | ||
context.nextRetry.value = now + nextRetry; | ||
} | ||
else { | ||
context.nextRetry.value = nextRetry; | ||
nextRetry = nextRetry - now; | ||
} | ||
if (nextRetry > 0) { | ||
await promisedTimeout(nextRetry); | ||
} | ||
} | ||
// is cancelled? | ||
if (context[CancellationToken].value) { | ||
return null; | ||
} | ||
// is our retry current one? | ||
if (retryId !== context[ExecutionId].value) { | ||
return result; | ||
} | ||
} while (i < MAX_RETRIES); | ||
return null; | ||
}; | ||
function useRetry(options, factory) { | ||
const opt = !options || isFunction(options) ? {} : options; | ||
const fn = isFunction(options) ? options : factory; | ||
if (!isFunction(options) && !isObject(options)) { | ||
throw new Error("[useRetry] options needs to be 'object'"); | ||
} | ||
if (!!fn && !isFunction(fn)) { | ||
throw new Error("[useRetry] factory needs to be 'function'"); | ||
} | ||
const isRetrying = compositionApi.ref(false); | ||
const nextRetry = compositionApi.ref(); | ||
const retryErrors = compositionApi.ref([]); | ||
const cancellationToken = { value: false }; | ||
const retryId = { value: 0 }; | ||
const retryCount = compositionApi.computed(() => retryErrors.value.length); | ||
const context = { | ||
isRetrying, | ||
retryCount, | ||
nextRetry, | ||
retryErrors, | ||
[ExecutionId]: retryId, | ||
[CancellationToken]: cancellationToken | ||
}; | ||
const exec = fn | ||
? (...args) => { | ||
++context[ExecutionId].value; | ||
return defaultStrategy(opt, context, fn, args); | ||
} | ||
: (f) => { | ||
++context[ExecutionId].value; | ||
return defaultStrategy(opt, context, f, undefined); | ||
}; | ||
const cancel = () => { | ||
context.isRetrying.value = false; | ||
context.retryErrors.value.push(new Error("[useRetry] cancelled")); | ||
context.nextRetry.value = undefined; | ||
cancellationToken.value = true; | ||
}; | ||
return { | ||
...context, | ||
cancel, | ||
exec | ||
}; | ||
} | ||
const exponentialDelay = retryNumber => { | ||
const delay = Math.pow(2, retryNumber) * 100; | ||
const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay | ||
return delay + randomSum; | ||
}; | ||
const noDelay = () => 0; | ||
function useFetch(options) { | ||
const json = compositionApi.ref(null); | ||
// TODO add text = ref<string> ?? | ||
const jsonError = compositionApi.ref(null); | ||
const use = usePromise(async (request) => { | ||
const isJson = options ? options.isJson !== false : true; | ||
const parseImmediate = options ? options.parseImmediate !== false : true; | ||
const use = usePromise(async (request, init) => { | ||
const response = await fetch(request, init); | ||
if (!init || init.isJson !== false) { | ||
if (isJson) { | ||
const pJson = response | ||
@@ -299,3 +458,3 @@ .json() | ||
}); | ||
if (!init || init.parseImmediate !== false) { | ||
if (parseImmediate) { | ||
await pJson; | ||
@@ -317,4 +476,140 @@ } | ||
/* istanbul ignore next */ | ||
const _axios = axios || (globalThis && globalThis.axios); | ||
function useAxios(config) { | ||
const axiosClient = _axios.create(config); | ||
const client = compositionApi.computed(() => axiosClient); | ||
const use = usePromise(async (request) => { | ||
return axiosClient.request(request); | ||
}); | ||
const data = compositionApi.computed(() => (use.result.value && use.result.value.data) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.data) || | ||
null); | ||
const status = compositionApi.computed(() => (use.result.value && use.result.value.status) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.status) || | ||
null); | ||
const statusText = compositionApi.computed(() => (use.result.value && use.result.value.statusText) || | ||
(use.error.value && | ||
use.error.value.response && | ||
use.error.value.response.statusText) || | ||
null); | ||
return { | ||
...use, | ||
client, | ||
data, | ||
status, | ||
statusText | ||
}; | ||
} | ||
function useWebSocket(url, protocols) { | ||
const ws = new WebSocket(url, protocols); | ||
const messageEvent = compositionApi.ref(null); | ||
const errorEvent = compositionApi.ref(); | ||
const data = compositionApi.ref(null); | ||
const isOpen = compositionApi.ref(false); | ||
const isClosed = compositionApi.ref(false); | ||
const errored = compositionApi.ref(false); | ||
ws.addEventListener("message", x => { | ||
messageEvent.value = x; | ||
data.value = x.data; | ||
}); | ||
ws.addEventListener("error", error => { | ||
errorEvent.value = error; | ||
errored.value = true; | ||
}); | ||
ws.addEventListener("close", () => { | ||
isOpen.value = false; | ||
isClosed.value = true; | ||
}); | ||
ws.addEventListener("open", () => { | ||
isOpen.value = true; | ||
isClosed.value = false; | ||
}); | ||
const send = (data) => ws.send(data); | ||
const close = (code, reason) => { | ||
ws.close(code, reason); | ||
}; | ||
return { | ||
ws, | ||
send, | ||
close, | ||
messageEvent, | ||
errorEvent, | ||
data, | ||
isOpen, | ||
isClosed, | ||
errored | ||
}; | ||
} | ||
// used to store all the instances of weakMap | ||
const keyedMap = new Map(); | ||
const weakMap = new WeakMap(); | ||
function useLocalStorage(key, defaultValue) { | ||
let lazy = false; | ||
let k = keyedMap.get(key); | ||
const json = localStorage.getItem(key); | ||
const storage = (k && weakMap.get(k)) || | ||
(!!defaultValue && wrap(defaultValue)) || | ||
compositionApi.ref(null); | ||
if (json && !k) { | ||
try { | ||
storage.value = JSON.parse(json); | ||
lazy = false; | ||
} | ||
catch (e) { | ||
/* istanbul ignore next */ | ||
console.warn("[useLocalStorage] error parsing value from localStorage", key, e); | ||
} | ||
} | ||
// do not watch if we already created the instance | ||
if (!k) { | ||
k = {}; | ||
keyedMap.set(key, k); | ||
weakMap.set(k, storage); | ||
compositionApi.watch(storage, storage => { | ||
if (storage === undefined) { | ||
localStorage.removeItem(key); | ||
return; | ||
} | ||
// do not overflow localStorage with updates nor keep doing stringify | ||
debounce(() => localStorage.setItem(key, JSON.stringify(storage)), 100)(); | ||
}, { | ||
deep: true, | ||
lazy | ||
}); | ||
} | ||
const clear = () => { | ||
keyedMap.forEach((v) => { | ||
const obj = weakMap.get(v); | ||
/* istanbul ignore else */ | ||
if (obj) { | ||
obj.value = undefined; | ||
} | ||
weakMap.delete(v); | ||
}); | ||
keyedMap.clear(); | ||
}; | ||
const remove = () => { | ||
keyedMap.delete(key); | ||
weakMap.delete(k); | ||
storage.value = undefined; | ||
}; | ||
return { | ||
storage, | ||
clear, | ||
remove | ||
}; | ||
} | ||
exports.debounce = debounce; | ||
exports.exponentialDelay = exponentialDelay; | ||
exports.noDelay = noDelay; | ||
exports.useArrayPagination = useArrayPagination; | ||
exports.useAxios = useAxios; | ||
exports.useCancellablePromise = useCancellablePromise; | ||
@@ -324,3 +619,4 @@ exports.useDebounce = useDebounce; | ||
exports.useFetch = useFetch; | ||
exports.useMouseMove = useMouseMove; | ||
exports.useLocalStorage = useLocalStorage; | ||
exports.useOnMouseMove = useOnMouseMove; | ||
exports.useOnResize = useOnResize; | ||
@@ -330,2 +626,4 @@ exports.useOnScroll = useOnScroll; | ||
exports.usePromise = usePromise; | ||
exports.useRetry = useRetry; | ||
exports.useWebSocket = useWebSocket; | ||
@@ -332,0 +630,0 @@ Object.defineProperty(exports, '__esModule', { value: true }); |
@@ -0,0 +0,0 @@ 'use strict' |
{ | ||
"name": "vue-composable", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "Vue composition-api composable components", | ||
@@ -27,3 +27,5 @@ "main": "index.js", | ||
"test:watch": "jest --coverage --watch", | ||
"test:prod": "npm run lint && npm run test -- --no-cache" | ||
"test:prod": "npm run lint && npm run test -- --no-cache", | ||
"docs:dev": "vuepress dev docs", | ||
"docs:build": "vuepress build docs" | ||
}, | ||
@@ -42,6 +44,11 @@ "bugs": { | ||
"@vue/composition-api": "^0.3.2", | ||
"@vuepress/plugin-back-to-top": "^1.2.0", | ||
"@vuepress/plugin-pwa": "^1.2.0", | ||
"axios": "^0.19.0", | ||
"coveralls": "^3.0.7", | ||
"jest": "^24.9.0", | ||
"jest-junit": "^9.0.0", | ||
"jest-websocket-mock": "^1.5.1", | ||
"lodash.camelcase": "^4.3.0", | ||
"mock-socket": "^9.0.2", | ||
"rimraf": "^3.0.0", | ||
@@ -55,7 +62,8 @@ "rollup": "^1.25.2", | ||
"rollup-plugin-terser": "^5.1.2", | ||
"rollup-plugin-typescript2": "^0.24.3", | ||
"rollup-plugin-typescript2": "^0.25.1", | ||
"ts-jest": "^24.1.0", | ||
"typescript": "^3.6.4", | ||
"vue": "^2.6.10" | ||
"vue": "^2.6.10", | ||
"vuepress": "^1.2.0" | ||
} | ||
} |
@@ -5,2 +5,3 @@ # vue-composable | ||
[![Coverage Status](https://coveralls.io/repos/github/pikax/vue-composable/badge.svg?branch=master)](https://coveralls.io/github/pikax/vue-composable?branch=master) | ||
[![npm version](https://badge.fury.io/js/vue-composable.svg)](https://badge.fury.io/js/vue-composable) | ||
@@ -11,27 +12,95 @@ ## Introduction | ||
100% typescript based composable components and full type support out-of-box. | ||
## NOTE | ||
Currently only works with [composition-api](https://github.com/vuejs/composition-api), when [Vue3](https://github.com/vuejs/vue-next) gets release I will update to use the new reactive system (using [@vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity)) | ||
## Installing | ||
```bash | ||
# install with yarn | ||
yarn add @vue/composition-api vue-composable | ||
| composable | description | example | extra | | ||
|---|---|---|---| | ||
| [useArrayPagination](src/arrayPagination.ts) | provides pagination for an array | [arrayPagination.html](examples/arrayPagination.html) | | | ||
| [useDebounce](src/debounce.ts) | debounces function | | | | ||
| [useEvent](src/event.ts) | handles the lifecycle of addEventListener/removeEventListener for a component. | | | | ||
| [useOnMouseMove](src/onMouseMove.ts) | gets data from element `movemove` | | | | ||
| [useOnResize](src/onResize.ts) | gets data from element `resize` | | | | ||
| [useOnScroll](src/onScroll.ts) | gets data from element `scroll` | | | | ||
| [usePagination](src/pagination.ts) | provides pagination controls. *NOTE: base type* | | | | ||
| [usePromise](src/promise.ts) | provides information about the state of the promise | | | | ||
| [useFetch](src/fetch.ts) | handles the fetch request | [fetch.html](examples/fetch.html) | | | ||
# install with npm | ||
npm install @vue/composition-api vue-composable | ||
``` | ||
## Documentation | ||
## Types | ||
Check our [documentation](https://pikax.me/vue-composable/) | ||
### Event | ||
- [Event](https://pikax.me/vue-composable/composable/event/event) - Attach event listener to a DOM element | ||
- [Mouse Move](https://pikax.me/vue-composable/composable/event/onMoveMove) - Attach `mousemove` listener to a DOM element | ||
- [Resize](https://pikax.me/vue-composable/composable/event/onResize) - Attach `resize` listener to a DOM element | ||
- [Scroll](https://pikax.me/vue-composable/composable/event/onScroll) - Attach `scroll` listener to a DOM element | ||
### MISC | ||
## License | ||
- [localStorage](https://pikax.me/vue-composable/composable/misc/localStorage) - Reactive access to a `localStorage` | ||
### Pagination | ||
- [Pagination](https://pikax.me/vue-composable/composable/pagination/pagination) - Generic reactive pagination controls | ||
- [Array Pagination](https://pikax.me/vue-composable/composable/pagination/arrayPagination) - Array pagination | ||
### Promise | ||
- [Promise](https://pikax.me/vue-composable/composable/promise/promise) - `Promise` reactive resolve and reject | ||
- [Cancellable Promise](https://pikax.me/vue-composable/composable/promise/cancellablePromise) - Allow to cancel promises | ||
- [Retry](https://pikax.me/vue-composable/composable/promise/retry) - Provides functionality to retry `promise` | ||
### Web | ||
- [Axios](https://pikax.me/vue-composable/composable/web/axios) - reactive `axios` wrapper client | ||
- [Fetch](https://pikax.me/vue-composable/composable/web/fetch) - reactive `fetch` wrapper | ||
- [WebSocket](https://pikax.me/vue-composable/composable/web/webSocket) - reactive `WebSocket` wrapper | ||
## Examples | ||
Check out the [examples folder](examples) or start hacking on [codesandbox](https://codesandbox.io/s/vue-composable-examples-yuusf). | ||
[![Edit Vue Composable Examples](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vue-template-yuusf?fontsize=14) | ||
### NOTE | ||
Currently only works with [composition-api](https://github.com/vuejs/composition-api), when [Vue3](https://github.com/vuejs/vue-next) gets release I will update to use the new reactive system (using [@vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity)) | ||
## Usage | ||
```vue | ||
<template> | ||
<div> | ||
<p>page {{ currentPage }} of {{ lastPage }}</p> | ||
<p> | ||
<button @click="prev">prev</button> | ||
<button @click="next">next</button> | ||
</p> | ||
<ul> | ||
<li v-for="n in result" :key="n"> | ||
{{ n }} | ||
</li> | ||
</ul> | ||
</div> | ||
</template> | ||
<script> | ||
import { useArrayPagination } from "vue-composable"; | ||
export default { | ||
setup() { | ||
const array = new Array(1000).fill(0).map((_, i) => i); | ||
const { result, next, prev, currentPage, lastPage } = useArrayPagination( | ||
array, | ||
{ | ||
pageSize: 3 | ||
} | ||
); | ||
return { result, next, prev, currentPage, lastPage }; | ||
} | ||
}; | ||
</script> | ||
``` | ||
## License | ||
[MIT](http://opensource.org/licenses/MIT) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
104498
2715
1
105
17
5
25
25