feat: add utils package
This commit is contained in:
15
packages/utils/src/Domain/Deferred/Deferred.ts
Normal file
15
packages/utils/src/Domain/Deferred/Deferred.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const Deferred = <T>() => {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void
|
||||
let reject!: () => void
|
||||
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res
|
||||
reject = rej
|
||||
})
|
||||
|
||||
return {
|
||||
resolve,
|
||||
reject,
|
||||
promise,
|
||||
}
|
||||
}
|
||||
61
packages/utils/src/Domain/Utils/Utils.spec.ts
Normal file
61
packages/utils/src/Domain/Utils/Utils.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as DOMPurifyLib from 'dompurify'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import { sortByKey, withoutLastElement } from './Utils'
|
||||
|
||||
const window = new JSDOM('').window
|
||||
const DOMPurify = DOMPurifyLib(window as never)
|
||||
|
||||
describe('Utils', () => {
|
||||
it('sanitizeHtmlString', () => {
|
||||
const dirty = '<svg><animate onbegin=alert(1) attributeName=x dur=1s>'
|
||||
const cleaned = DOMPurify.sanitize(dirty)
|
||||
expect(cleaned).toEqual('<svg></svg>')
|
||||
})
|
||||
|
||||
it('without last works', () => {
|
||||
expect(withoutLastElement([])).toEqual([])
|
||||
expect(withoutLastElement(['a'])).toEqual([])
|
||||
expect(withoutLastElement(['a', 'b'])).toEqual(['a'])
|
||||
expect(withoutLastElement(['a', 'b', 'c'])).toEqual(['a', 'b'])
|
||||
})
|
||||
|
||||
const sortByKey_INPUT = [
|
||||
{ id: 'aza', age: 0, missing: 7 },
|
||||
{ id: 'aaa', age: 12, missing: 8 },
|
||||
{ id: 'ace', age: 12, missing: 0 },
|
||||
{ id: 'zzz', age: -9 },
|
||||
]
|
||||
|
||||
it('sortByKey sort by key', () => {
|
||||
const input = sortByKey_INPUT
|
||||
|
||||
expect(sortByKey(input, 'id')).toEqual([
|
||||
{ id: 'aaa', age: 12, missing: 8 },
|
||||
{ id: 'ace', age: 12, missing: 0 },
|
||||
{ id: 'aza', age: 0, missing: 7 },
|
||||
{ id: 'zzz', age: -9 },
|
||||
])
|
||||
|
||||
expect(sortByKey(input, 'age')).toEqual([
|
||||
{ id: 'zzz', age: -9 },
|
||||
{ id: 'aza', age: 0, missing: 7 },
|
||||
{ id: 'aaa', age: 12, missing: 8 },
|
||||
{ id: 'ace', age: 12, missing: 0 },
|
||||
])
|
||||
|
||||
expect(sortByKey(input, 'missing')).toEqual([
|
||||
{ id: 'ace', age: 12, missing: 0 },
|
||||
{ id: 'aza', age: 0, missing: 7 },
|
||||
{ id: 'aaa', age: 12, missing: 8 },
|
||||
{ id: 'zzz', age: -9 },
|
||||
])
|
||||
})
|
||||
|
||||
it('sortByKey does not mutate the input & creates a new array', () => {
|
||||
const input = sortByKey_INPUT
|
||||
const initial = [...input]
|
||||
|
||||
expect(sortByKey(input, 'id')).not.toBe(input)
|
||||
expect(initial).toEqual(input)
|
||||
})
|
||||
})
|
||||
681
packages/utils/src/Domain/Utils/Utils.ts
Normal file
681
packages/utils/src/Domain/Utils/Utils.ts
Normal file
@@ -0,0 +1,681 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import * as DOMPurify from 'dompurify'
|
||||
import { find, isArray, mergeWith, remove, uniq, uniqWith } from 'lodash'
|
||||
import { AnyRecord } from '@standardnotes/common'
|
||||
|
||||
const collator = typeof Intl !== 'undefined' ? new Intl.Collator('en', { numeric: true }) : undefined
|
||||
|
||||
export function getGlobalScope(): Window | unknown | null {
|
||||
return typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null
|
||||
}
|
||||
|
||||
export function dictToArray<T>(dict: Record<any, T>): T[] {
|
||||
return Object.values(dict)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we are in a web browser
|
||||
*/
|
||||
export function isWebEnvironment(): boolean {
|
||||
return getGlobalScope() !== null
|
||||
}
|
||||
|
||||
interface IEDocument {
|
||||
documentMode?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if WebCrypto is available
|
||||
*/
|
||||
export function isWebCryptoAvailable(): boolean {
|
||||
return (
|
||||
(isWebEnvironment() && !isReactNativeEnvironment() && !(document && (document as IEDocument).documentMode)) ||
|
||||
(/Edge/.test(navigator.userAgent) && window.crypto && !!window.crypto.subtle)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we are in React Native app
|
||||
*/
|
||||
export function isReactNativeEnvironment(): boolean {
|
||||
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative'
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches array of objects for first object where object[key] === value
|
||||
* @returns Matching object or null if not found
|
||||
*/
|
||||
export function findInArray<T, K extends keyof T>(array: T[], key: K, value: T[K]): T | undefined {
|
||||
return array.find((item: T) => item[key] === value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches array of objects for first object where object[key] === value
|
||||
* @returns Matching object or null if not found
|
||||
*/
|
||||
export function searchArray<T>(array: T[], predicate: Partial<T>): T | undefined {
|
||||
return find(array, predicate) as T
|
||||
}
|
||||
|
||||
export function sureSearchArray<T>(array: T[], predicate: Partial<T>): T {
|
||||
return searchArray(array, predicate) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Whether the value is a function or object
|
||||
*/
|
||||
export function isObject(value: unknown): value is object {
|
||||
if (value === null) {
|
||||
return false
|
||||
}
|
||||
return typeof value === 'function' || typeof value === 'object'
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Whether the value is a function
|
||||
*/
|
||||
export function isFunction(value: unknown): boolean {
|
||||
if (value === null) {
|
||||
return false
|
||||
}
|
||||
return typeof value === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns True if the object is null or undefined, otherwise false
|
||||
*/
|
||||
export function isNullOrUndefined(value: unknown): value is null | undefined {
|
||||
return value === null || value === undefined
|
||||
}
|
||||
|
||||
export function isNotUndefined<T>(val: T | undefined | null): val is T {
|
||||
return val != undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns True if the string is empty or undefined
|
||||
*/
|
||||
export function isEmpty(string: string): boolean {
|
||||
return !string || string.length === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Whether the value is a string
|
||||
*/
|
||||
export function isString(value: unknown): value is string {
|
||||
return typeof value === 'string' || value instanceof String
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The greater of the two dates
|
||||
*/
|
||||
export function greaterOfTwoDates(dateA: Date, dateB: Date): Date {
|
||||
if (dateA > dateB) {
|
||||
return dateA
|
||||
} else {
|
||||
return dateB
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array containing only unique values by combining the two input arrays.
|
||||
* Elements are unique based on the values of `equalityKeys`.
|
||||
* @param equalityKeys - Keys to determine element equality
|
||||
* @returns Array containing unique values
|
||||
*/
|
||||
export function uniqCombineObjArrays<T>(arrayA: T[], arrayB: T[], equalityKeys: (keyof T)[]): T[] {
|
||||
return uniqWith(arrayA.concat(arrayB), (a: T, b: T) => {
|
||||
for (const key of equalityKeys) {
|
||||
if (a[key] !== b[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array containing only unique values
|
||||
* @returns Array containing unique values
|
||||
*/
|
||||
export function uniqueArray<T>(array: T[]): T[] {
|
||||
return uniq(array)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array containing only unique values
|
||||
* @returns Array containing unique values
|
||||
*/
|
||||
export function uniqueArrayByKey<T>(array: T[], key: keyof T): T[] {
|
||||
return uniqWith(array, (a: T, b: T) => {
|
||||
return a[key] === b[key]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last element in the array.
|
||||
* @returns The last element in the array
|
||||
*/
|
||||
export function lastElement<T>(array: T[]): T | undefined {
|
||||
return array[array.length - 1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all items from otherArray into inArray, in-place.
|
||||
* Does not return a value.
|
||||
*/
|
||||
export function extendArray<T, K extends T>(inArray: T[], otherArray: K[]): void {
|
||||
for (const value of otherArray) {
|
||||
inArray.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all items appearing in toSubtract from inArray, in-place
|
||||
* @param toSubtract - The list of items to remove from inArray
|
||||
*/
|
||||
export function subtractFromArray<T>(inArray: T[], toSubtract: T[]): void {
|
||||
for (const value of toSubtract) {
|
||||
removeFromArray(inArray, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the first matching element of an array by strict equality.
|
||||
* If no matchin element is found, the array is left unchanged.
|
||||
*/
|
||||
export function removeFromArray<T>(array: T[], value: T): void {
|
||||
const valueIndex = array.indexOf(value)
|
||||
if (valueIndex === -1) {
|
||||
return
|
||||
}
|
||||
array.splice(valueIndex, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the element to the array if the array does not already include the value.
|
||||
* The array is searched via array.indexOf
|
||||
* @returns true if value was added
|
||||
*/
|
||||
export function addIfUnique<T>(array: T[], value: T): boolean {
|
||||
if (!array.includes(value)) {
|
||||
array.push(value)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an object from the array in-place by searching for an object where all the
|
||||
* key/values in predicate match with the candidate element.
|
||||
*/
|
||||
export function filterFromArray<T>(
|
||||
array: T[],
|
||||
predicate: Partial<Record<keyof T, any>> | ((object: T) => boolean),
|
||||
): void {
|
||||
remove(array, predicate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array by removing all elements in subtract from array
|
||||
*/
|
||||
export function arrayByDifference<T>(array: T[], subtract: T[]): T[] {
|
||||
return array.filter((x) => !subtract.includes(x)).concat(subtract.filter((x) => !array.includes(x)))
|
||||
}
|
||||
|
||||
export function compareValues<T>(left: T, right: T) {
|
||||
if ((left && !right) || (!left && right)) {
|
||||
return false
|
||||
}
|
||||
if (left instanceof Date && right instanceof Date) {
|
||||
return left.getTime() === right.getTime()
|
||||
} else if (left instanceof String && right instanceof String) {
|
||||
return left === right
|
||||
} else {
|
||||
return topLevelCompare(left, right)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the value from the array at the given index, in-place.
|
||||
*/
|
||||
export function removeFromIndex(array: any[], index: number) {
|
||||
array.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the value from the array at the given index, in-place.
|
||||
*/
|
||||
export function addAtIndex<T>(array: T[], element: T, index: number) {
|
||||
array.splice(index, 0, element)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array by removeing the value from the array at the given index
|
||||
*/
|
||||
export function arrayByRemovingFromIndex<T>(array: T[], index: number) {
|
||||
const copy = array.slice()
|
||||
removeFromIndex(copy, index)
|
||||
return copy
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array where each element is the value of a top-level
|
||||
* object key.
|
||||
* Example: objectToValueArray({a: 1, b: 2}) returns [1, 2]
|
||||
*/
|
||||
export function objectToValueArray(object: AnyRecord) {
|
||||
const values = []
|
||||
for (const key of Object.keys(object)) {
|
||||
values.push(object[key])
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a key-sorted copy of the object.
|
||||
* For example, sortedCopy({b: '1', a: '2'}) returns {a: '2', b: '1'}
|
||||
*/
|
||||
export function sortedCopy(object: any) {
|
||||
const keys = Object.keys(object).sort()
|
||||
const result: any = {}
|
||||
for (const key of keys) {
|
||||
result[key] = object[key]
|
||||
}
|
||||
return Copy(result)
|
||||
}
|
||||
|
||||
export const sortByKey = <T>(input: T[], key: keyof T): T[] => {
|
||||
const compare = (a: T, b: T): number => {
|
||||
const valueA = a[key]
|
||||
const valueB = b[key]
|
||||
|
||||
if (valueA < valueB) {
|
||||
return -1
|
||||
}
|
||||
if (valueA > valueB) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const newArray = [...input]
|
||||
newArray.sort(compare)
|
||||
|
||||
return newArray
|
||||
}
|
||||
|
||||
/** Returns a new object by omitting any keys which have an undefined or null value */
|
||||
export function omitUndefinedCopy(object: any) {
|
||||
const result: any = {}
|
||||
for (const key of Object.keys(object)) {
|
||||
if (!isNullOrUndefined(object[key])) {
|
||||
result[key] = object[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array by sorting an array of elements based on a date property,
|
||||
* as indicated by the input key value.
|
||||
*/
|
||||
export function dateSorted<T>(elements: T[], key: keyof T, ascending = true) {
|
||||
return elements.sort((a, b) => {
|
||||
const aTimestamp = (a[key] as unknown as Date).getTime()
|
||||
const bTimestamp = (b[key] as unknown as Date).getTime()
|
||||
const vector = ascending ? 1 : -1
|
||||
if (aTimestamp < bTimestamp) {
|
||||
return -1 * vector
|
||||
} else if (aTimestamp > bTimestamp) {
|
||||
return 1 * vector
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Compares for equality by comparing top-level keys value equality (===) */
|
||||
export function topLevelCompare<T>(left: T, right: T) {
|
||||
if (!left && !right) {
|
||||
return true
|
||||
}
|
||||
if (!left || !right) {
|
||||
return false
|
||||
}
|
||||
const leftKeys = Object.keys(left)
|
||||
const rightKeys = Object.keys(right)
|
||||
if (leftKeys.length !== rightKeys.length) {
|
||||
return false
|
||||
}
|
||||
for (const key of leftKeys) {
|
||||
if ((left as any)[key] !== (right as any)[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new object by attempting to JSON.parse any top-level object keys.
|
||||
*/
|
||||
export function jsonParseEmbeddedKeys(object: AnyRecord) {
|
||||
const result: AnyRecord = {}
|
||||
for (const key of Object.keys(object)) {
|
||||
let value
|
||||
try {
|
||||
value = JSON.parse(object[key] as string)
|
||||
} catch (error) {
|
||||
value = object[key]
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const withoutLastElement = <T>(array: T[]): T[] => {
|
||||
return array.slice(0, -1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes keys of the input object.
|
||||
*/
|
||||
export function omitInPlace<T>(object: T, keys: Array<keyof T>) {
|
||||
if (!object) {
|
||||
return
|
||||
}
|
||||
for (const key of keys) {
|
||||
delete object[key]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new object by omitting `keys` from `object`
|
||||
*/
|
||||
export function omitByCopy<T>(object: T, keys: Array<keyof T>) {
|
||||
if (isNullOrUndefined(object)) {
|
||||
return undefined
|
||||
}
|
||||
const newObject = Object.assign({}, object)
|
||||
/**
|
||||
* Lodash's omit, which was previously used, seems to cause unexpected behavior
|
||||
* when payload is an ES6 item class. So we instead manually omit each key.
|
||||
*/
|
||||
for (const key of keys) {
|
||||
delete newObject[key]
|
||||
}
|
||||
return newObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Similiar to Node's path.join, this function combines an array of paths into
|
||||
* one resolved path.
|
||||
*/
|
||||
export function joinPaths(...args: string[]) {
|
||||
return args
|
||||
.map((part, i) => {
|
||||
if (i === 0) {
|
||||
return part.trim().replace(/[/]*$/g, '')
|
||||
} else {
|
||||
return part.trim().replace(/(^[/]*|[/]*$)/g, '')
|
||||
}
|
||||
})
|
||||
.filter((x) => x.length)
|
||||
.join('/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the input object by JSON stringifying the object then JSON parsing
|
||||
* the string (if the input is an object). If input is date, a Date copy will be created,
|
||||
* and if input is a primitive value, it will be returned as-is.
|
||||
*/
|
||||
export function Copy(object: any) {
|
||||
if (object instanceof Date) {
|
||||
return new Date(object)
|
||||
} else if (isObject(object)) {
|
||||
return JSON.parse(JSON.stringify(object))
|
||||
} else {
|
||||
return object
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the second object parameter into the first object, in-place.
|
||||
* @returns The now modified first object parameter passed into the function.
|
||||
*/
|
||||
export function deepMerge(a: AnyRecord, b: AnyRecord) {
|
||||
/**
|
||||
* lodash.merge will not merge a full array with an empty one.
|
||||
* deepMerge will replace arrays wholesale
|
||||
*/
|
||||
if (!a || !b) {
|
||||
throw 'Attempting to deepMerge with null values'
|
||||
}
|
||||
const customizer = (aValue: any, bValue: any) => {
|
||||
if (isArray(aValue)) {
|
||||
return bValue
|
||||
}
|
||||
}
|
||||
mergeWith(a, b, customizer)
|
||||
return a
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new object by selecting certain keys from input object.
|
||||
*/
|
||||
export function pickByCopy<T>(object: T, keys: Array<keyof T>) {
|
||||
const result = {} as T
|
||||
for (const key of keys) {
|
||||
result[key] = object[key]
|
||||
}
|
||||
return Copy(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively makes an object immutable via Object.freeze
|
||||
*/
|
||||
export function deepFreeze(object: any) {
|
||||
const propNames = Object.getOwnPropertyNames(object)
|
||||
for (const name of propNames) {
|
||||
const value = object[name]
|
||||
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
||||
object[name] = deepFreeze(value)
|
||||
} else {
|
||||
object[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze(object)
|
||||
}
|
||||
|
||||
export function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an object has a getter defined for a given property
|
||||
*/
|
||||
export function hasGetter(object: any, property: string) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(object), property)
|
||||
return descriptor && !isNullOrUndefined(descriptor.get)
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a hex string into a desired number of bits
|
||||
* @returns A hexadecimal string truncated to the number of desired bits
|
||||
*/
|
||||
export function truncateHexString(string: string, desiredBits: number) {
|
||||
const BITS_PER_HEX_CHAR = 4
|
||||
const desiredCharLength = desiredBits / BITS_PER_HEX_CHAR
|
||||
return string.substring(0, desiredCharLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* When awaited, this function allows code execution to pause for a set time.
|
||||
* Should be used primarily for testing.
|
||||
*/
|
||||
export async function sleep(milliseconds: number, warn = true): Promise<void> {
|
||||
if (warn) {
|
||||
console.warn(`Sleeping for ${milliseconds}ms`)
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(function () {
|
||||
resolve()
|
||||
}, milliseconds)
|
||||
})
|
||||
}
|
||||
|
||||
export function assertUnreachable(uncheckedCase: never): never {
|
||||
throw Error('Unchecked case ' + uncheckedCase)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean representing whether two dates are on the same day
|
||||
*/
|
||||
export function isSameDay(dateA: Date, dateB: Date) {
|
||||
return (
|
||||
dateA.getFullYear() === dateB.getFullYear() &&
|
||||
dateA.getMonth() === dateB.getMonth() &&
|
||||
dateA.getDate() === dateB.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts an array of objects in natural order
|
||||
* @param items - The array of objects to sort
|
||||
* @param property - The objects' property to sort by
|
||||
* @param direction - The sorting direction, either ascending (default) or descending
|
||||
* @returns Array of objects sorted in natural order
|
||||
*/
|
||||
export function naturalSort<T extends AnyRecord>(
|
||||
items: T[],
|
||||
property: keyof T,
|
||||
direction: 'asc' | 'desc' = 'asc',
|
||||
): T[] {
|
||||
switch (direction) {
|
||||
case 'asc':
|
||||
return [...items].sort(
|
||||
collator
|
||||
? (a, b) => collator.compare(a[property] as string, b[property] as string)
|
||||
: (a, b) => (a[property] as string).localeCompare(b[property] as string, 'en', { numeric: true }),
|
||||
)
|
||||
case 'desc':
|
||||
return [...items].sort(
|
||||
collator
|
||||
? (a, b) => collator.compare(b[property] as string, a[property] as string)
|
||||
: (a, b) => (b[property] as string).localeCompare(a[property] as string, 'en', { numeric: true }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function arraysEqual<T>(left: T[], right: T[]): boolean {
|
||||
if (left.length !== right.length) {
|
||||
return false
|
||||
}
|
||||
return left.every((item) => right.includes(item)) && right.every((item) => left.includes(item))
|
||||
}
|
||||
|
||||
const MicrosecondsInAMillisecond = 1_000
|
||||
const MillisecondsInASecond = 1_000
|
||||
|
||||
enum TimestampDigits {
|
||||
Seconds = 10,
|
||||
Milliseconds = 13,
|
||||
Microseconds = 16,
|
||||
}
|
||||
|
||||
export function convertTimestampToMilliseconds(timestamp: number): number {
|
||||
const digits = String(timestamp).length
|
||||
switch (digits) {
|
||||
case TimestampDigits.Seconds:
|
||||
return timestamp * MillisecondsInASecond
|
||||
case TimestampDigits.Milliseconds:
|
||||
return timestamp
|
||||
case TimestampDigits.Microseconds:
|
||||
return Math.floor(timestamp / MicrosecondsInAMillisecond)
|
||||
|
||||
default:
|
||||
throw `Unhandled timestamp precision: ${timestamp}`
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeHtmlString(html: string): string {
|
||||
return DOMPurify.sanitize(html)
|
||||
}
|
||||
|
||||
let sharedDateFormatter: unknown
|
||||
export function dateToLocalizedString(date: Date): string {
|
||||
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat && typeof navigator !== 'undefined') {
|
||||
if (!sharedDateFormatter) {
|
||||
const locale = navigator.languages && navigator.languages.length ? navigator.languages[0] : navigator.language
|
||||
sharedDateFormatter = new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
weekday: 'long',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
return (sharedDateFormatter as Intl.DateTimeFormat).format(date)
|
||||
} else {
|
||||
// IE < 11, Safari <= 9.0.
|
||||
// In English, this generates the string most similar to
|
||||
// the toLocaleDateString() result above.
|
||||
return date.toDateString() + ' ' + date.toLocaleTimeString()
|
||||
}
|
||||
}
|
||||
|
||||
export function nonSecureRandomIdentifier(): string {
|
||||
return `${Math.random() * 100}`.replace('.', '')
|
||||
}
|
||||
|
||||
export function splitString(string: string, parts: number): string[] {
|
||||
const outputLength = string.length
|
||||
const partLength = outputLength / parts
|
||||
const partitions = []
|
||||
for (let i = 0; i < parts; i++) {
|
||||
const partition = string.slice(partLength * i, partLength * (i + 1))
|
||||
partitions.push(partition)
|
||||
}
|
||||
return partitions
|
||||
}
|
||||
|
||||
export function firstHalfOfString(string: string): string {
|
||||
return string.substring(0, string.length / 2)
|
||||
}
|
||||
|
||||
export function secondHalfOfString(string: string): string {
|
||||
return string.substring(string.length / 2, string.length)
|
||||
}
|
||||
|
||||
export function log(namespace: string, ...args: any[]): void {
|
||||
const date = new Date()
|
||||
const timeString = `${date.toLocaleTimeString().replace(' PM', '').replace(' AM', '')}.${date.getMilliseconds()}`
|
||||
customLog(
|
||||
`%c${namespace}%c${timeString}`,
|
||||
'color: black; font-weight: bold; margin-right: 4px',
|
||||
'color: gray',
|
||||
...args,
|
||||
)
|
||||
}
|
||||
|
||||
function customLog(..._args: any[]) {
|
||||
// eslint-disable-next-line no-console, prefer-rest-params
|
||||
Function.prototype.apply.call(console.log, console, arguments)
|
||||
}
|
||||
|
||||
export function assert(value: unknown): asserts value {
|
||||
if (value === undefined) {
|
||||
throw new Error('Assertion failed; value must be defined')
|
||||
}
|
||||
}
|
||||
|
||||
export function useBoolean(value: boolean | undefined, defaultValue: boolean): boolean {
|
||||
return value != undefined ? value : defaultValue
|
||||
}
|
||||
5
packages/utils/src/Domain/Uuid/Utils.ts
Normal file
5
packages/utils/src/Domain/Uuid/Utils.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function Uuids(items: { uuid: string }[]): string[] {
|
||||
return items.map((item) => {
|
||||
return item.uuid
|
||||
})
|
||||
}
|
||||
21
packages/utils/src/Domain/Uuid/UuidGenerator.ts
Normal file
21
packages/utils/src/Domain/Uuid/UuidGenerator.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* An abstract class with no instance methods. Used globally to generate uuids by any
|
||||
* consumer. Application must call SetGenerator before use.
|
||||
*/
|
||||
export class UuidGenerator {
|
||||
private static syncUuidFunc: () => string
|
||||
|
||||
/**
|
||||
* @param {function} syncImpl - A syncronous function that returns a UUID.
|
||||
*/
|
||||
static SetGenerator(syncImpl: () => string): void {
|
||||
this.syncUuidFunc = syncImpl
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a UUID string asyncronously.
|
||||
*/
|
||||
public static GenerateUuid(): string {
|
||||
return this.syncUuidFunc()
|
||||
}
|
||||
}
|
||||
95
packages/utils/src/Domain/Uuid/UuidMap.ts
Normal file
95
packages/utils/src/Domain/Uuid/UuidMap.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { addIfUnique, removeFromArray } from '../Utils/Utils'
|
||||
|
||||
/**
|
||||
* Maps a UUID to an array of UUIDS to establish either direct or inverse
|
||||
* relationships between UUID strings (represantative of items or payloads).
|
||||
*/
|
||||
export class UuidMap {
|
||||
/** uuid to uuids that we have a relationship with */
|
||||
private directMap: Partial<Record<Uuid, Uuid[]>> = {}
|
||||
/** uuid to uuids that have a relationship with us */
|
||||
private inverseMap: Partial<Record<Uuid, Uuid[]>> = {}
|
||||
|
||||
public makeCopy(): UuidMap {
|
||||
const copy = new UuidMap()
|
||||
copy.directMap = Object.assign({}, this.directMap)
|
||||
copy.inverseMap = Object.assign({}, this.inverseMap)
|
||||
return copy
|
||||
}
|
||||
|
||||
public getDirectRelationships(uuid: Uuid): Uuid[] {
|
||||
return this.directMap[uuid] || []
|
||||
}
|
||||
|
||||
public getInverseRelationships(uuid: Uuid): Uuid[] {
|
||||
return this.inverseMap[uuid] || []
|
||||
}
|
||||
|
||||
public establishRelationship(uuidA: Uuid, uuidB: Uuid): void {
|
||||
this.establishDirectRelationship(uuidA, uuidB)
|
||||
this.establishInverseRelationship(uuidA, uuidB)
|
||||
}
|
||||
|
||||
public deestablishRelationship(uuidA: Uuid, uuidB: Uuid): void {
|
||||
this.deestablishDirectRelationship(uuidA, uuidB)
|
||||
this.deestablishInverseRelationship(uuidA, uuidB)
|
||||
}
|
||||
|
||||
public setAllRelationships(uuid: Uuid, relationships: Uuid[]): void {
|
||||
const previousDirect = this.directMap[uuid] || []
|
||||
this.directMap[uuid] = relationships
|
||||
|
||||
/** Remove all previous values in case relationships have changed
|
||||
* The updated references will be added afterwards.
|
||||
*/
|
||||
for (const previousRelationship of previousDirect) {
|
||||
this.deestablishInverseRelationship(uuid, previousRelationship)
|
||||
}
|
||||
|
||||
/** Now map current relationships */
|
||||
for (const newRelationship of relationships) {
|
||||
this.establishInverseRelationship(uuid, newRelationship)
|
||||
}
|
||||
}
|
||||
|
||||
public removeFromMap(uuid: Uuid): void {
|
||||
/** Items that we reference */
|
||||
const directReferences = this.directMap[uuid] || []
|
||||
for (const directReference of directReferences) {
|
||||
removeFromArray(this.inverseMap[directReference] || [], uuid)
|
||||
}
|
||||
delete this.directMap[uuid]
|
||||
|
||||
/** Items that are referencing us */
|
||||
const inverseReferences = this.inverseMap[uuid] || []
|
||||
for (const inverseReference of inverseReferences) {
|
||||
removeFromArray(this.directMap[inverseReference] || [], uuid)
|
||||
}
|
||||
delete this.inverseMap[uuid]
|
||||
}
|
||||
|
||||
private establishDirectRelationship(uuidA: Uuid, uuidB: Uuid): void {
|
||||
const index = this.directMap[uuidA] || []
|
||||
addIfUnique(index, uuidB)
|
||||
this.directMap[uuidA] = index
|
||||
}
|
||||
|
||||
private establishInverseRelationship(uuidA: Uuid, uuidB: Uuid): void {
|
||||
const inverseIndex = this.inverseMap[uuidB] || []
|
||||
addIfUnique(inverseIndex, uuidA)
|
||||
this.inverseMap[uuidB] = inverseIndex
|
||||
}
|
||||
|
||||
private deestablishDirectRelationship(uuidA: Uuid, uuidB: Uuid): void {
|
||||
const index = this.directMap[uuidA] || []
|
||||
removeFromArray(index, uuidB)
|
||||
this.directMap[uuidA] = index
|
||||
}
|
||||
|
||||
private deestablishInverseRelationship(uuidA: Uuid, uuidB: Uuid): void {
|
||||
const inverseIndex = this.inverseMap[uuidB] || []
|
||||
removeFromArray(inverseIndex, uuidA)
|
||||
this.inverseMap[uuidB] = inverseIndex
|
||||
}
|
||||
}
|
||||
5
packages/utils/src/Domain/index.ts
Normal file
5
packages/utils/src/Domain/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './Utils/Utils'
|
||||
export * from './Uuid/UuidGenerator'
|
||||
export * from './Uuid/UuidMap'
|
||||
export * from './Uuid/Utils'
|
||||
export * from './Deferred/Deferred'
|
||||
Reference in New Issue
Block a user