@bessemer/cornerstone 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/jest.config.js +3 -0
- package/package.json +39 -0
- package/src/array.ts +142 -0
- package/src/async.ts +114 -0
- package/src/cache.ts +236 -0
- package/src/combinable.ts +40 -0
- package/src/comparator.ts +78 -0
- package/src/content.ts +138 -0
- package/src/context.ts +6 -0
- package/src/crypto.ts +11 -0
- package/src/date.ts +18 -0
- package/src/duration.ts +57 -0
- package/src/either.ts +29 -0
- package/src/entry.ts +21 -0
- package/src/equalitor.ts +12 -0
- package/src/error-event.ts +126 -0
- package/src/error.ts +16 -0
- package/src/expression/array-expression.ts +29 -0
- package/src/expression/expression-evaluator.ts +34 -0
- package/src/expression/expression.ts +188 -0
- package/src/expression/internal.ts +34 -0
- package/src/expression/numeric-expression.ts +182 -0
- package/src/expression/string-expression.ts +38 -0
- package/src/expression.ts +48 -0
- package/src/function.ts +3 -0
- package/src/glob.ts +19 -0
- package/src/global-variable.ts +40 -0
- package/src/hash.ts +28 -0
- package/src/hex-code.ts +6 -0
- package/src/index.ts +82 -0
- package/src/lazy.ts +11 -0
- package/src/logger.ts +144 -0
- package/src/math.ts +132 -0
- package/src/misc.ts +22 -0
- package/src/object.ts +236 -0
- package/src/patch.ts +128 -0
- package/src/precondition.ts +25 -0
- package/src/promise.ts +16 -0
- package/src/property.ts +29 -0
- package/src/reference.ts +68 -0
- package/src/resource.ts +32 -0
- package/src/result.ts +66 -0
- package/src/retry.ts +70 -0
- package/src/rich-text.ts +24 -0
- package/src/set.ts +46 -0
- package/src/signature.ts +20 -0
- package/src/store.ts +91 -0
- package/src/string.ts +173 -0
- package/src/tag.ts +68 -0
- package/src/types.ts +21 -0
- package/src/ulid.ts +28 -0
- package/src/unit.ts +4 -0
- package/src/uri.ts +321 -0
- package/src/url.ts +155 -0
- package/src/uuid.ts +37 -0
- package/src/zod.ts +24 -0
- package/test/comparator.test.ts +1 -0
- package/test/expression.test.ts +12 -0
- package/test/object.test.ts +104 -0
- package/test/patch.test.ts +170 -0
- package/test/set.test.ts +20 -0
- package/test/string.test.ts +22 -0
- package/test/uri.test.ts +111 -0
- package/test/url.test.ts +174 -0
- package/tsconfig.build.json +13 -0
- package/tsup.config.ts +4 -0
package/src/property.ts
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
import { Objects, Tags } from '@bessemer/cornerstone'
|
2
|
+
import { SerializedTags, Tag, TaggedValue } from '@bessemer/cornerstone/tag'
|
3
|
+
import { UnknownRecord } from 'type-fest'
|
4
|
+
import { DeepPartial } from '@bessemer/cornerstone/types'
|
5
|
+
|
6
|
+
export type PropertyRecord<T extends UnknownRecord> = {
|
7
|
+
values: T
|
8
|
+
overrides: Record<SerializedTags, PropertyOverride<T>>
|
9
|
+
}
|
10
|
+
|
11
|
+
export type PropertyOverride<T> = TaggedValue<DeepPartial<T>>
|
12
|
+
|
13
|
+
export const properties = <T extends UnknownRecord>(values: T, overrides?: Array<PropertyOverride<T>>): PropertyRecord<T> => {
|
14
|
+
const propertyOverrideEntries = (overrides ?? []).map((override) => {
|
15
|
+
return [Tags.serializeTags(override.tags), override]
|
16
|
+
})
|
17
|
+
|
18
|
+
const propertyOverrides: Record<SerializedTags, PropertyOverride<T>> = Object.fromEntries(propertyOverrideEntries)
|
19
|
+
|
20
|
+
return {
|
21
|
+
values,
|
22
|
+
overrides: propertyOverrides,
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
export const resolve = <T extends UnknownRecord>(properties: PropertyRecord<T>, tags: Array<Tag>): T => {
|
27
|
+
const overrides = Tags.resolve(Object.values(properties.overrides), tags)
|
28
|
+
return Objects.mergeAll([properties.values, ...overrides.reverse()]) as T
|
29
|
+
}
|
package/src/reference.ts
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
import { Comparators, Equalitors, Objects, Strings } from '@bessemer/cornerstone'
|
2
|
+
import { Comparator } from '@bessemer/cornerstone/comparator'
|
3
|
+
import { NominalType } from '@bessemer/cornerstone/types'
|
4
|
+
|
5
|
+
export type ReferenceId<T extends string> = NominalType<string, ['ReferenceId', T]>
|
6
|
+
|
7
|
+
export type Reference<T extends string> = {
|
8
|
+
id: ReferenceId<T>
|
9
|
+
type: T
|
10
|
+
note?: string
|
11
|
+
}
|
12
|
+
|
13
|
+
export type ReferenceType<T extends Reference<string>> = T | T['id']
|
14
|
+
|
15
|
+
export interface Referencable<T extends Reference<string>> {
|
16
|
+
reference: T
|
17
|
+
}
|
18
|
+
|
19
|
+
export type ReferencableType<T extends Reference<string>> = T | Referencable<T>
|
20
|
+
|
21
|
+
export const reference = <T extends string>(reference: Reference<T> | ReferenceId<T>, type: T, note?: string): Reference<typeof type> => {
|
22
|
+
if (!Strings.isString(reference)) {
|
23
|
+
return reference
|
24
|
+
}
|
25
|
+
|
26
|
+
return {
|
27
|
+
id: reference,
|
28
|
+
type,
|
29
|
+
...(Objects.isPresent(note) ? { note: note } : {}),
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
export const isReferencable = (element: unknown): element is Referencable<Reference<string>> => {
|
34
|
+
if (!Objects.isObject(element)) {
|
35
|
+
return false
|
36
|
+
}
|
37
|
+
|
38
|
+
const referencable = element as Referencable<Reference<string>>
|
39
|
+
return !Objects.isUndefined(referencable.reference)
|
40
|
+
}
|
41
|
+
|
42
|
+
export const isReference = (element: unknown): element is Reference<string> => {
|
43
|
+
if (!Objects.isObject(element)) {
|
44
|
+
return false
|
45
|
+
}
|
46
|
+
|
47
|
+
const referencable = element as Reference<string>
|
48
|
+
return !Objects.isUndefined(referencable.id) && !Objects.isUndefined(referencable.type) && !Objects.isUndefined(referencable.note)
|
49
|
+
}
|
50
|
+
|
51
|
+
export const getReference = <T extends Reference<string>>(reference: ReferencableType<T>): T => {
|
52
|
+
const referencable = reference as Referencable<T>
|
53
|
+
if (Objects.isPresent(referencable.reference)) {
|
54
|
+
return referencable.reference
|
55
|
+
} else {
|
56
|
+
return reference as T
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
export const equals = <T extends string>(first: Reference<T>, second: Reference<T>): boolean => {
|
61
|
+
return first.id === second.id
|
62
|
+
}
|
63
|
+
|
64
|
+
export const comparator = <T extends string>(): Comparator<Reference<T>> => {
|
65
|
+
return Comparators.compareBy((it) => it.id, Comparators.natural())
|
66
|
+
}
|
67
|
+
|
68
|
+
export const equalitor = () => Equalitors.fromComparator(comparator())
|
package/src/resource.ts
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
import { NominalType } from '@bessemer/cornerstone/types'
|
2
|
+
import { Strings, Zod } from '@bessemer/cornerstone/index'
|
3
|
+
import { ZodType } from 'zod'
|
4
|
+
|
5
|
+
export type ResourceKey = string
|
6
|
+
|
7
|
+
export type ResourceNamespace = NominalType<string, 'ResourceNamespace'>
|
8
|
+
export const ResourceNamespaceSchema: ZodType<ResourceNamespace> = Zod.string()
|
9
|
+
|
10
|
+
export namespace ResourceKey {
|
11
|
+
const ResourceNamespaceSeparator = '/'
|
12
|
+
|
13
|
+
export const namespace = (namespace: ResourceNamespace, key: ResourceKey): ResourceKey => {
|
14
|
+
return `${namespace}${ResourceNamespaceSeparator}${key}`
|
15
|
+
}
|
16
|
+
|
17
|
+
export const namespaceAll = (namespace: ResourceNamespace, keys: Array<ResourceKey>): Array<ResourceKey> => {
|
18
|
+
return keys.map((it) => ResourceKey.namespace(namespace, it))
|
19
|
+
}
|
20
|
+
|
21
|
+
export const stripNamespace = (namespace: ResourceNamespace, key: ResourceKey): ResourceKey => {
|
22
|
+
return Strings.removeStart(key, `${namespace}${ResourceNamespaceSeparator}`)
|
23
|
+
}
|
24
|
+
|
25
|
+
export const stripNamespaceAll = (namespace: ResourceNamespace, keys: Array<ResourceKey>): Array<ResourceKey> => {
|
26
|
+
return keys.map((it) => ResourceKey.stripNamespace(namespace, it))
|
27
|
+
}
|
28
|
+
|
29
|
+
export const extendNamespace = (...namespaces: Array<ResourceNamespace>): ResourceNamespace => {
|
30
|
+
return namespaces.join(ResourceNamespaceSeparator)
|
31
|
+
}
|
32
|
+
}
|
package/src/result.ts
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
import { Left, Right } from '@bessemer/cornerstone/either'
|
2
|
+
import { Eithers, Promises } from '@bessemer/cornerstone'
|
3
|
+
import { Throwable } from '@bessemer/cornerstone/types'
|
4
|
+
|
5
|
+
export type Success<T> = Right<T> & {
|
6
|
+
isSuccess: true
|
7
|
+
}
|
8
|
+
|
9
|
+
export type Failure = Left<Throwable | null> & {
|
10
|
+
isSuccess: false
|
11
|
+
}
|
12
|
+
|
13
|
+
export type Result<T> = Success<T> | Failure
|
14
|
+
export type AsyncResult<T> = Promise<Result<T>>
|
15
|
+
|
16
|
+
export const success = <T>(value: T): Success<T> => {
|
17
|
+
return { ...Eithers.right(value), isSuccess: true }
|
18
|
+
}
|
19
|
+
|
20
|
+
export const failure = (failure?: Throwable): Failure => {
|
21
|
+
return { ...Eithers.left(failure ?? null), isSuccess: false }
|
22
|
+
}
|
23
|
+
|
24
|
+
export function tryValue<SOURCE_VALUE>(resolver: () => Promise<SOURCE_VALUE>): AsyncResult<SOURCE_VALUE>
|
25
|
+
export function tryValue<SOURCE_VALUE>(resolver: () => SOURCE_VALUE): Result<SOURCE_VALUE>
|
26
|
+
export function tryValue<SOURCE_VALUE>(resolver: () => SOURCE_VALUE | Promise<SOURCE_VALUE>): Result<SOURCE_VALUE> | Promise<Result<SOURCE_VALUE>> {
|
27
|
+
try {
|
28
|
+
let result = resolver()
|
29
|
+
if (Promises.isPromise(result)) {
|
30
|
+
return result.then((it) => success(it)).catch((it) => failure(it))
|
31
|
+
} else {
|
32
|
+
return success(result)
|
33
|
+
}
|
34
|
+
} catch (e: any) {
|
35
|
+
return failure(e)
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
export function tryResult<SOURCE_VALUE>(resolver: () => Result<SOURCE_VALUE>): Result<SOURCE_VALUE>
|
40
|
+
export function tryResult<SOURCE_VALUE>(resolver: () => AsyncResult<SOURCE_VALUE>): AsyncResult<SOURCE_VALUE>
|
41
|
+
export function tryResult<SOURCE_VALUE>(resolver: () => Result<SOURCE_VALUE> | AsyncResult<SOURCE_VALUE>): ReturnType<typeof resolver> {
|
42
|
+
try {
|
43
|
+
let result = resolver()
|
44
|
+
if (Promises.isPromise(result)) {
|
45
|
+
return result.catch((it) => failure(it))
|
46
|
+
} else {
|
47
|
+
return result
|
48
|
+
}
|
49
|
+
} catch (e: any) {
|
50
|
+
return failure(e)
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
export function map<SOURCE_VALUE, TARGET_VALUE>(
|
55
|
+
result: Result<SOURCE_VALUE>,
|
56
|
+
valueMapper: (element: SOURCE_VALUE) => TARGET_VALUE
|
57
|
+
): Result<TARGET_VALUE> {
|
58
|
+
if (result.isSuccess) {
|
59
|
+
return {
|
60
|
+
...result,
|
61
|
+
value: valueMapper(result.value),
|
62
|
+
}
|
63
|
+
} else {
|
64
|
+
return result
|
65
|
+
}
|
66
|
+
}
|
package/src/retry.ts
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
import { Duration } from '@bessemer/cornerstone/duration'
|
2
|
+
import { Async, Durations, Maths, Objects, Preconditions, Results } from '@bessemer/cornerstone/index'
|
3
|
+
import { AsyncResult, Result } from '@bessemer/cornerstone/result'
|
4
|
+
import { PartialDeep } from 'type-fest'
|
5
|
+
|
6
|
+
export type RetryProps = {
|
7
|
+
attempts: number
|
8
|
+
delay: Duration
|
9
|
+
}
|
10
|
+
|
11
|
+
export type RetryOptions = PartialDeep<RetryProps>
|
12
|
+
|
13
|
+
export const None: RetryProps = {
|
14
|
+
attempts: 0,
|
15
|
+
delay: Durations.Zero,
|
16
|
+
}
|
17
|
+
|
18
|
+
export const DefaultRetryProps: RetryProps = {
|
19
|
+
attempts: 3,
|
20
|
+
delay: Durations.ofMilliseconds(500),
|
21
|
+
}
|
22
|
+
|
23
|
+
export type RetryState = {
|
24
|
+
attempt: number
|
25
|
+
props: RetryProps
|
26
|
+
}
|
27
|
+
|
28
|
+
export const initialize = (initialOptions?: RetryOptions): RetryState => {
|
29
|
+
const props = Objects.merge(DefaultRetryProps, initialOptions)
|
30
|
+
Preconditions.isTrue(props.attempts >= 0, () => 'usingRetry attempts must be >= 0')
|
31
|
+
|
32
|
+
return {
|
33
|
+
attempt: 0,
|
34
|
+
props,
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
export const retry = async (state: RetryState): Promise<RetryState | undefined> => {
|
39
|
+
if (state.attempt >= state.props.attempts - 1) {
|
40
|
+
return undefined
|
41
|
+
}
|
42
|
+
|
43
|
+
const delayMs = Durations.inMilliseconds(state.props.delay)
|
44
|
+
const maxJitterMs = delayMs * 0.3 // We calculate max jitter as 30% of the delay
|
45
|
+
await Async.sleep(Durations.ofMilliseconds(delayMs + Maths.random(0, maxJitterMs)))
|
46
|
+
|
47
|
+
return {
|
48
|
+
props: state.props,
|
49
|
+
attempt: state.attempt + 1,
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
export const usingRetry = async <T>(runnable: () => Promise<Result<T>>, initialOptions?: RetryOptions): AsyncResult<T> => {
|
54
|
+
let retryState: RetryState | undefined = initialize(initialOptions)
|
55
|
+
let previousResult: Result<T> = Results.failure()
|
56
|
+
|
57
|
+
do {
|
58
|
+
// JOHN Should this be a try/catch? it was causing debugging problems
|
59
|
+
const result = await runnable()
|
60
|
+
previousResult = result
|
61
|
+
|
62
|
+
if (result.isSuccess) {
|
63
|
+
return result
|
64
|
+
}
|
65
|
+
|
66
|
+
retryState = await retry(retryState)
|
67
|
+
} while (!Objects.isUndefined(retryState))
|
68
|
+
|
69
|
+
return previousResult
|
70
|
+
}
|
package/src/rich-text.ts
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
import { NominalType } from '@bessemer/cornerstone/types'
|
2
|
+
import { Objects } from '@bessemer/cornerstone'
|
3
|
+
|
4
|
+
// These are to match TipTap types, but without us having to depend on the TipTap library
|
5
|
+
export type RichTextJson = {
|
6
|
+
type?: string
|
7
|
+
attrs?: Record<string, any>
|
8
|
+
content?: RichTextJson[]
|
9
|
+
marks?: {
|
10
|
+
type: string
|
11
|
+
attrs?: Record<string, any>
|
12
|
+
[key: string]: any
|
13
|
+
}[]
|
14
|
+
text?: string
|
15
|
+
[key: string]: any
|
16
|
+
}
|
17
|
+
|
18
|
+
export type RichTextString = NominalType<string, 'RichTextString'>
|
19
|
+
|
20
|
+
export type RichText = RichTextString | RichTextJson
|
21
|
+
|
22
|
+
export const isJson = (text: RichText): text is RichTextJson => {
|
23
|
+
return Objects.isObject(text)
|
24
|
+
}
|
package/src/set.ts
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
export const cartesianProduct = <T>(...arrays: Array<Array<T>>): Array<Array<T>> => {
|
2
|
+
return arrays.reduce<Array<Array<T>>>((acc, array) => acc.flatMap((product) => array.map((element) => [...product, element])), [[]])
|
3
|
+
}
|
4
|
+
|
5
|
+
export const permute = <T>(values: Array<T>): Array<Array<T>> => {
|
6
|
+
let result: Array<Array<T>> = []
|
7
|
+
|
8
|
+
const permuteInternal = (arr: Array<T>, m: Array<T> = []) => {
|
9
|
+
if (arr.length === 0) {
|
10
|
+
result.push(m)
|
11
|
+
} else {
|
12
|
+
for (let i = 0; i < arr.length; i++) {
|
13
|
+
let curr = arr.slice()
|
14
|
+
let next = curr.splice(i, 1)
|
15
|
+
permuteInternal(curr.slice(), m.concat(next))
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
permuteInternal(values)
|
21
|
+
return result
|
22
|
+
}
|
23
|
+
|
24
|
+
export const properPowerSet = <T>(values: Array<T>): Array<Array<T>> => {
|
25
|
+
const powerSet: Array<Array<T>> = []
|
26
|
+
|
27
|
+
const totalSubsets = 1 << values.length // 2^n, where n is the size of the set
|
28
|
+
|
29
|
+
for (let i = 1; i < totalSubsets; i++) {
|
30
|
+
// Start from 1 to exclude the empty set
|
31
|
+
const subset: T[] = []
|
32
|
+
for (let j = 0; j < values.length; j++) {
|
33
|
+
if (i & (1 << j)) {
|
34
|
+
// Check if the j-th element is in the subset
|
35
|
+
subset.push(values[j]!)
|
36
|
+
}
|
37
|
+
}
|
38
|
+
powerSet.push(subset)
|
39
|
+
}
|
40
|
+
|
41
|
+
return powerSet
|
42
|
+
}
|
43
|
+
|
44
|
+
export const powerSet = <T>(values: Array<T>): Array<Array<T>> => {
|
45
|
+
return [[], ...properPowerSet(values)]
|
46
|
+
}
|
package/src/signature.ts
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
import { Objects, References } from '@bessemer/cornerstone'
|
2
|
+
import { Reference } from '@bessemer/cornerstone/reference'
|
3
|
+
|
4
|
+
export type Signable = number | string | { id: string } | { reference: Reference<string> }
|
5
|
+
|
6
|
+
export const sign = (value: Signable): string | number => {
|
7
|
+
if (Objects.isObject(value)) {
|
8
|
+
if (References.isReferencable(value)) {
|
9
|
+
return value.reference.id
|
10
|
+
} else {
|
11
|
+
return value.id
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
return value
|
16
|
+
}
|
17
|
+
|
18
|
+
export const signAll = (values: Array<Signable>): Array<string | number> => {
|
19
|
+
return values.map(sign)
|
20
|
+
}
|
package/src/store.ts
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
import { ResourceKey } from '@bessemer/cornerstone/resource'
|
2
|
+
import { Entry } from '@bessemer/cornerstone/entry'
|
3
|
+
import { Arrays, Entries, Objects } from '@bessemer/cornerstone/index'
|
4
|
+
|
5
|
+
export interface LocalStore<T> {
|
6
|
+
setValue: (value: T | undefined) => void
|
7
|
+
getValue: () => T | undefined
|
8
|
+
}
|
9
|
+
|
10
|
+
export interface LocalKeyValueStore<T> {
|
11
|
+
setValues: (entries: Array<Entry<T | undefined>>) => void
|
12
|
+
setValue: (key: ResourceKey, value: T | undefined) => void
|
13
|
+
|
14
|
+
getEntries: () => Array<Entry<T>>
|
15
|
+
getValues: (keys: Array<ResourceKey>) => Array<Entry<T>>
|
16
|
+
getValue: (key: ResourceKey) => T | undefined
|
17
|
+
}
|
18
|
+
|
19
|
+
export interface RemoteStore<T> {
|
20
|
+
writeValue: (value: T | undefined) => Promise<void>
|
21
|
+
fetchValue: () => Promise<T | undefined>
|
22
|
+
}
|
23
|
+
|
24
|
+
export interface RemoteKeyValueStore<T> {
|
25
|
+
writeValues: (entries: Array<Entry<T | undefined>>) => Promise<void>
|
26
|
+
writeValue: (key: ResourceKey, value: T | undefined) => Promise<void>
|
27
|
+
|
28
|
+
fetchValues: (keys: Array<ResourceKey>) => Promise<Array<Entry<T>>>
|
29
|
+
fetchValue: (key: ResourceKey) => Promise<T | undefined>
|
30
|
+
}
|
31
|
+
|
32
|
+
export abstract class AbstractRemoteKeyValueStore<T> implements RemoteKeyValueStore<T> {
|
33
|
+
abstract writeValues: (entries: Array<Entry<T | undefined>>) => Promise<void>
|
34
|
+
|
35
|
+
writeValue = async (key: ResourceKey, value: T | undefined): Promise<void> => {
|
36
|
+
await this.writeValues([Entries.of(key, value)])
|
37
|
+
}
|
38
|
+
|
39
|
+
abstract fetchValues: (keys: Array<ResourceKey>) => Promise<Array<Entry<T>>>
|
40
|
+
|
41
|
+
fetchValue = async (key: ResourceKey): Promise<T | undefined> => {
|
42
|
+
return Arrays.first(await this.fetchValues([key]))?.[1]
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
export abstract class AbstractLocalKeyValueStore<T> extends AbstractRemoteKeyValueStore<T> implements LocalKeyValueStore<T> {
|
47
|
+
abstract setValues: (entries: Array<Entry<T | undefined>>) => void
|
48
|
+
|
49
|
+
setValue = (key: ResourceKey, value: T | undefined): void => {
|
50
|
+
this.setValues([Entries.of(key, value)])
|
51
|
+
}
|
52
|
+
|
53
|
+
abstract getEntries: () => Array<Entry<T>>
|
54
|
+
abstract getValues: (keys: Array<ResourceKey>) => Array<Entry<T>>
|
55
|
+
|
56
|
+
getValue = (key: ResourceKey): T | undefined => {
|
57
|
+
return Arrays.first(this.getValues([key]))?.[1]
|
58
|
+
}
|
59
|
+
|
60
|
+
fetchValues = async (keys: Array<ResourceKey>): Promise<Array<Entry<T>>> => {
|
61
|
+
return this.getValues(keys)
|
62
|
+
}
|
63
|
+
|
64
|
+
writeValues = async (entries: Array<Entry<T | undefined>>): Promise<void> => {
|
65
|
+
this.setValues(entries)
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
export const fromMap = <T>(): LocalKeyValueStore<T> => {
|
70
|
+
const map = new Map<string, T>()
|
71
|
+
|
72
|
+
return new (class extends AbstractLocalKeyValueStore<T> {
|
73
|
+
override getEntries = (): Array<Entry<T>> => {
|
74
|
+
return Array.from(map.entries())
|
75
|
+
}
|
76
|
+
|
77
|
+
override setValues = (entries: Entry<T | undefined>[]): void => {
|
78
|
+
entries.forEach(([key, value]) => {
|
79
|
+
if (Objects.isNil(value)) {
|
80
|
+
map.delete(key)
|
81
|
+
} else {
|
82
|
+
map.set(key, value)
|
83
|
+
}
|
84
|
+
})
|
85
|
+
}
|
86
|
+
|
87
|
+
override getValues = (keys: Array<ResourceKey>): Array<Entry<T>> => {
|
88
|
+
return keys.map((key) => Entries.of(key, map.get(key)!)).filter((it) => Objects.isPresent(it[1]))
|
89
|
+
}
|
90
|
+
})()
|
91
|
+
}
|
package/src/string.ts
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
import {
|
2
|
+
endsWith as _endsWith,
|
3
|
+
includes as _includes,
|
4
|
+
isEmpty as _isEmpty,
|
5
|
+
isString as _isString,
|
6
|
+
padEnd as _padEnd,
|
7
|
+
padStart as _padStart,
|
8
|
+
replace as _replace,
|
9
|
+
startsWith as _startsWith,
|
10
|
+
} from 'lodash-es'
|
11
|
+
import { Arrays } from '@bessemer/cornerstone'
|
12
|
+
import { UnknownRecord } from 'type-fest'
|
13
|
+
|
14
|
+
export const isString = (value?: any): value is string => {
|
15
|
+
return _isString(value)
|
16
|
+
}
|
17
|
+
|
18
|
+
export const isEmpty = _isEmpty
|
19
|
+
|
20
|
+
export type StringSplitResult = { selection: string; separator: string; rest: string } | { selection: null; separator: null; rest: string }
|
21
|
+
|
22
|
+
export const splitFirst = (str: string, splitter: string | RegExp): StringSplitResult => {
|
23
|
+
if (isString(splitter)) {
|
24
|
+
const results = str.split(splitter)
|
25
|
+
if (results.length === 1) {
|
26
|
+
return { selection: null, separator: null, rest: str }
|
27
|
+
}
|
28
|
+
|
29
|
+
return { selection: results[0]!, separator: splitter, rest: Arrays.rest(results).join(splitter) }
|
30
|
+
} else {
|
31
|
+
const match = splitter.exec(str)
|
32
|
+
|
33
|
+
if (!match) {
|
34
|
+
return { selection: null, separator: null, rest: str }
|
35
|
+
}
|
36
|
+
|
37
|
+
const matchIndex = match.index
|
38
|
+
const beforeMatch = str.slice(0, matchIndex)
|
39
|
+
const afterMatch = str.slice(matchIndex + match[0].length)
|
40
|
+
const separator = match[0]
|
41
|
+
return { selection: beforeMatch, separator, rest: afterMatch }
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
export const splitLast = (str: string, splitter: string | RegExp): StringSplitResult => {
|
46
|
+
if (isString(splitter)) {
|
47
|
+
const results = str.split(splitter)
|
48
|
+
if (results.length === 1) {
|
49
|
+
return { selection: null, separator: null, rest: str }
|
50
|
+
}
|
51
|
+
|
52
|
+
return { selection: results[results.length - 1]!, separator: splitter, rest: results.slice(0, results.length - 1).join(splitter) }
|
53
|
+
} else {
|
54
|
+
if (!splitter.global) {
|
55
|
+
splitter = new RegExp(splitter.source, splitter.flags + 'g')
|
56
|
+
}
|
57
|
+
|
58
|
+
const matches = Array.from(str.matchAll(splitter))
|
59
|
+
|
60
|
+
if (matches.length === 0) {
|
61
|
+
return { selection: null, separator: null, rest: str }
|
62
|
+
}
|
63
|
+
|
64
|
+
// Use the last match
|
65
|
+
const lastMatch = matches[matches.length - 1]!
|
66
|
+
const matchIndex = lastMatch.index!
|
67
|
+
const separator = lastMatch[0]!
|
68
|
+
const beforeMatch = str.slice(0, matchIndex)
|
69
|
+
const afterMatch = str.slice(matchIndex + separator.length)
|
70
|
+
|
71
|
+
return {
|
72
|
+
selection: afterMatch,
|
73
|
+
separator: separator,
|
74
|
+
rest: beforeMatch,
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
export const splitLastRegex = (str: string, regex: RegExp): StringSplitResult => {
|
80
|
+
// Find the last match using regex lastIndex
|
81
|
+
let lastMatch: RegExpExecArray | null = null
|
82
|
+
let match
|
83
|
+
|
84
|
+
while ((match = regex.exec(str)) !== null) {
|
85
|
+
lastMatch = match
|
86
|
+
}
|
87
|
+
|
88
|
+
if (!lastMatch) {
|
89
|
+
return { selection: null, separator: null, rest: str }
|
90
|
+
}
|
91
|
+
|
92
|
+
const matchIndex = lastMatch.index!
|
93
|
+
const separator = lastMatch[0]
|
94
|
+
const beforeMatch = str.slice(0, matchIndex)
|
95
|
+
const afterMatch = str.slice(matchIndex + separator.length)
|
96
|
+
|
97
|
+
return {
|
98
|
+
selection: afterMatch,
|
99
|
+
separator,
|
100
|
+
rest: beforeMatch,
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
export const splitAt = (str: string, index: number): [string, string] => {
|
105
|
+
return [str.slice(0, index), str.slice(index)] as const
|
106
|
+
}
|
107
|
+
|
108
|
+
export const startsWith = _startsWith
|
109
|
+
export const endsWith = _endsWith
|
110
|
+
|
111
|
+
export const removeStart = (string: string, substring: string): string => {
|
112
|
+
if (!string.startsWith(substring)) {
|
113
|
+
return string
|
114
|
+
}
|
115
|
+
|
116
|
+
return string.slice(substring.length)
|
117
|
+
}
|
118
|
+
|
119
|
+
export const removeEnd = (string: string, substring: string): string => {
|
120
|
+
if (!string.endsWith(substring)) {
|
121
|
+
return string
|
122
|
+
}
|
123
|
+
|
124
|
+
return string.slice(0, -substring.length)
|
125
|
+
}
|
126
|
+
|
127
|
+
export const isBlank = (str?: string | null): boolean => {
|
128
|
+
const testStr = str ?? ''
|
129
|
+
return /^\s*$/.test(testStr)
|
130
|
+
}
|
131
|
+
|
132
|
+
export const mostCentralOccurrence = (str: string, substr: string): number | null => {
|
133
|
+
const occurrences: number[] = []
|
134
|
+
let index = str.indexOf(substr)
|
135
|
+
|
136
|
+
while (index !== -1) {
|
137
|
+
occurrences.push(index)
|
138
|
+
index = str.indexOf(substr, index + 1)
|
139
|
+
}
|
140
|
+
|
141
|
+
if (occurrences.length === 0) {
|
142
|
+
return null
|
143
|
+
}
|
144
|
+
|
145
|
+
const center = str.length / 2
|
146
|
+
|
147
|
+
let closestIndex = occurrences[0]!
|
148
|
+
let minDistance = Math.abs(center - closestIndex)
|
149
|
+
|
150
|
+
for (let i = 1; i < occurrences.length; i++) {
|
151
|
+
const distance = Math.abs(center - occurrences[i]!)
|
152
|
+
if (distance < minDistance) {
|
153
|
+
minDistance = distance
|
154
|
+
closestIndex = occurrences[i]!
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
return closestIndex
|
159
|
+
}
|
160
|
+
|
161
|
+
export const replacePlaceholders = (string: string, parameters: UnknownRecord, getParamPlaceholder = (paramName: string) => `{{${paramName}}}`) => {
|
162
|
+
return Object.entries(parameters).reduce(
|
163
|
+
(intermediateString, [paramName, paramValue]) => intermediateString.replaceAll(getParamPlaceholder(paramName), `${paramValue}`),
|
164
|
+
string
|
165
|
+
)
|
166
|
+
}
|
167
|
+
|
168
|
+
export const padStart = _padStart
|
169
|
+
export const padEnd = _padEnd
|
170
|
+
|
171
|
+
export const contains = _includes
|
172
|
+
|
173
|
+
export const replace = _replace
|
package/src/tag.ts
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
import { NominalType } from '@bessemer/cornerstone/types'
|
2
|
+
import { Comparator } from '@bessemer/cornerstone/comparator'
|
3
|
+
import { Arrays, Comparators, Equalitors, Sets } from '@bessemer/cornerstone'
|
4
|
+
import { Equalitor } from '@bessemer/cornerstone/equalitor'
|
5
|
+
|
6
|
+
export type TagType<DataType> = NominalType<string, ['TagType', DataType]>
|
7
|
+
|
8
|
+
export type Tag<DataType = unknown> = {
|
9
|
+
type: TagType<DataType>
|
10
|
+
value: DataType
|
11
|
+
}
|
12
|
+
|
13
|
+
export type TaggedValue<T> = {
|
14
|
+
value: T
|
15
|
+
tags: Array<Tag>
|
16
|
+
}
|
17
|
+
|
18
|
+
export const tag = <T>(type: TagType<T>, value: T): Tag<T> => {
|
19
|
+
return {
|
20
|
+
type,
|
21
|
+
value,
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
export const value = <T>(value: T, tags: Array<Tag>) => {
|
26
|
+
return {
|
27
|
+
value,
|
28
|
+
tags: Arrays.sortWith(tags, tagComparator()),
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
export const tagComparator = <T>(): Comparator<Tag<T>> => {
|
33
|
+
return Comparators.aggregate([
|
34
|
+
Comparators.compareBy((it) => it.type, Comparators.natural()),
|
35
|
+
Comparators.compareBy((it) => JSON.stringify(it.value), Comparators.natural()),
|
36
|
+
])
|
37
|
+
}
|
38
|
+
|
39
|
+
export const tagEqualitor = <T>(): Equalitor<Tag<T>> => {
|
40
|
+
return Equalitors.fromComparator(tagComparator())
|
41
|
+
}
|
42
|
+
|
43
|
+
export type SerializedTags = NominalType<string, 'SerializedTags'>
|
44
|
+
|
45
|
+
export const serializeTags = <T>(tags: Array<Tag<T>>): SerializedTags => {
|
46
|
+
const serializedTags: SerializedTags = Arrays.sortWith(tags, tagComparator())
|
47
|
+
.map(({ type, value }) => `${type}:${JSON.stringify(value)}`)
|
48
|
+
.join('.')
|
49
|
+
|
50
|
+
return serializedTags
|
51
|
+
}
|
52
|
+
|
53
|
+
export const resolve = <T>(values: Array<TaggedValue<T>>, tags: Array<Tag>): Array<TaggedValue<T>> => {
|
54
|
+
return resolveBy(values, (it) => it.tags, tags)
|
55
|
+
}
|
56
|
+
|
57
|
+
export const resolveBy = <T>(values: Array<T>, mapper: (value: T) => Array<Tag>, tags: Array<Tag>): Array<T> => {
|
58
|
+
const resolvedValues = Sets.properPowerSet(tags).flatMap((tags) => {
|
59
|
+
return values.filter((it) => Arrays.equalWith(mapper(it), tags, tagEqualitor()))
|
60
|
+
})
|
61
|
+
|
62
|
+
if (Arrays.isEmpty(resolvedValues)) {
|
63
|
+
const defaultValues = values.filter((it) => Arrays.isEmpty(mapper(it)))
|
64
|
+
return defaultValues
|
65
|
+
}
|
66
|
+
|
67
|
+
return resolvedValues.reverse()
|
68
|
+
}
|