@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/jest.config.js
ADDED
package/package.json
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
{
|
2
|
+
"name": "@bessemer/cornerstone",
|
3
|
+
"type": "module",
|
4
|
+
"version": "0.5.0",
|
5
|
+
"main": "./dist/index.cjs",
|
6
|
+
"module": "./dist/index.js",
|
7
|
+
"types": "./dist/index.d.ts",
|
8
|
+
"sideEffects": false,
|
9
|
+
"exports": {
|
10
|
+
".": {
|
11
|
+
"import": "./dist/index.js",
|
12
|
+
"types": "./dist/index.d.ts"
|
13
|
+
},
|
14
|
+
"./*": {
|
15
|
+
"import": "./dist/*.js",
|
16
|
+
"types": "./dist/*.d.ts"
|
17
|
+
}
|
18
|
+
},
|
19
|
+
"scripts": {
|
20
|
+
"build": "tsup && tsc --project tsconfig.build.json --emitDeclarationOnly",
|
21
|
+
"test": "jest",
|
22
|
+
"prettier-fix": "prettier --write .",
|
23
|
+
"clean": "rm -rf dist && rm -rf .turbo"
|
24
|
+
},
|
25
|
+
"dependencies": {
|
26
|
+
"date-fns": "4.1.0",
|
27
|
+
"immer": "10.1.1",
|
28
|
+
"lodash-es": "4.17.21",
|
29
|
+
"minimatch": "10.0.1",
|
30
|
+
"pino": "9.6.0",
|
31
|
+
"type-fest": "4.32.0",
|
32
|
+
"ulid": "2.3.0",
|
33
|
+
"zod": "3.24.2"
|
34
|
+
},
|
35
|
+
"devDependencies": {
|
36
|
+
"@types/lodash-es": "4.17.12",
|
37
|
+
"pino-pretty": "13.0.0"
|
38
|
+
}
|
39
|
+
}
|
package/src/array.ts
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
import {
|
2
|
+
concat,
|
3
|
+
differenceBy as _differenceBy,
|
4
|
+
differenceWith as _differenceWith,
|
5
|
+
first as _first,
|
6
|
+
flatten as _flatten,
|
7
|
+
groupBy as _groupBy,
|
8
|
+
isEmpty as _isEmpty,
|
9
|
+
last as _last,
|
10
|
+
range as _range,
|
11
|
+
remove as _remove,
|
12
|
+
uniqBy,
|
13
|
+
uniqWith,
|
14
|
+
} from 'lodash-es'
|
15
|
+
import { Equalitor } from '@bessemer/cornerstone/equalitor'
|
16
|
+
import { Signable } from '@bessemer/cornerstone/signature'
|
17
|
+
import { Comparators, Eithers, Preconditions, Signatures } from '@bessemer/cornerstone'
|
18
|
+
import { Either } from '@bessemer/cornerstone/either'
|
19
|
+
import { Comparator } from '@bessemer/cornerstone/comparator'
|
20
|
+
import { Arrayable } from 'type-fest'
|
21
|
+
|
22
|
+
export const equalWith = <T>(first: Array<T>, second: Array<T>, equalitor: Equalitor<T>): boolean => {
|
23
|
+
if (first.length !== second.length) {
|
24
|
+
return false
|
25
|
+
}
|
26
|
+
|
27
|
+
return first.every((element, index) => equalitor(element, second[index]!))
|
28
|
+
}
|
29
|
+
|
30
|
+
export const equalBy = <T>(first: Array<T>, second: Array<T>, mapper: (element: T) => Signable): boolean => {
|
31
|
+
return equalWith(first, second, (first, second) => Signatures.sign(mapper(first)) === Signatures.sign(mapper(second)))
|
32
|
+
}
|
33
|
+
|
34
|
+
export const equal = <T extends Signable>(first: Array<T>, second: Array<T>): boolean => {
|
35
|
+
return equalBy(first, second, Signatures.sign)
|
36
|
+
}
|
37
|
+
|
38
|
+
export const differenceWith = <T>(first: Array<T>, second: Array<T>, equalitor: Equalitor<T>): Array<T> => {
|
39
|
+
return _differenceWith(first, second, equalitor)
|
40
|
+
}
|
41
|
+
|
42
|
+
export const differenceBy = <T>(first: Array<T>, second: Array<T>, mapper: (element: T) => Signable): Array<T> => {
|
43
|
+
return _differenceBy(first, second, (it) => Signatures.sign(mapper(it)))
|
44
|
+
}
|
45
|
+
|
46
|
+
export const difference = <T extends Signable>(first: Array<T>, second: Array<T>): Array<T> => {
|
47
|
+
return differenceBy(first, second, Signatures.sign)
|
48
|
+
}
|
49
|
+
|
50
|
+
export const removeWith = <T>(array: Array<T>, element: T, equalitor: Equalitor<T>): Array<T> => {
|
51
|
+
return differenceWith(array, [element], equalitor)
|
52
|
+
}
|
53
|
+
|
54
|
+
export const removeBy = <T>(array: Array<T>, element: T, mapper: (element: T) => Signable): Array<T> => {
|
55
|
+
return differenceBy(array, [element], mapper)
|
56
|
+
}
|
57
|
+
|
58
|
+
export const remove = <T extends Signable>(array: Array<T>, element: T): Array<T> => {
|
59
|
+
return difference(array, [element])
|
60
|
+
}
|
61
|
+
|
62
|
+
export const containsWith = <T>(array: Array<T>, element: T, equalitor: Equalitor<T>): boolean => {
|
63
|
+
return array.some((it) => equalitor(it, element))
|
64
|
+
}
|
65
|
+
|
66
|
+
export const containsBy = <T>(array: Array<T>, element: T, mapper: (element: T) => Signable): boolean => {
|
67
|
+
return containsWith(array, element, (first, second) => Signatures.sign(mapper(first)) === Signatures.sign(mapper(second)))
|
68
|
+
}
|
69
|
+
|
70
|
+
export const contains = <T extends Signable>(array: Array<T>, element: T): boolean => containsBy(array, element, Signatures.sign)
|
71
|
+
|
72
|
+
export const containsAllWith = <T>(first: Array<T>, second: Array<T>, equalitor: Equalitor<T>): boolean =>
|
73
|
+
isEmpty(differenceWith(second, first, equalitor))
|
74
|
+
|
75
|
+
export const containsAllBy = <T>(first: Array<T>, second: Array<T>, mapper: (element: T) => Signable): boolean =>
|
76
|
+
isEmpty(differenceBy(second, first, mapper))
|
77
|
+
|
78
|
+
export const containsAll = <T extends Signable>(first: Array<T>, second: Array<T>): boolean => isEmpty(difference(second, first))
|
79
|
+
|
80
|
+
export const dedupeWith = <T>(array: Array<T>, equalitor: Equalitor<T>): Array<T> => {
|
81
|
+
return uniqWith(array, equalitor)
|
82
|
+
}
|
83
|
+
|
84
|
+
export const dedupeBy = <T>(array: Array<T>, mapper: (element: T) => Signable): Array<T> => {
|
85
|
+
return uniqBy(array, (it) => Signatures.sign(mapper(it)))
|
86
|
+
}
|
87
|
+
|
88
|
+
export const dedupe = <T extends Signable>(array: Array<T>): Array<T> => {
|
89
|
+
return dedupeBy(array, Signatures.sign)
|
90
|
+
}
|
91
|
+
|
92
|
+
export const sortWith = <T>(array: Array<T>, comparator: Comparator<T>): Array<T> => {
|
93
|
+
return [...array].sort(comparator)
|
94
|
+
}
|
95
|
+
|
96
|
+
export const sortBy = <T>(array: Array<T>, mapper: (element: T) => Signable): Array<T> => {
|
97
|
+
return sortWith(
|
98
|
+
array,
|
99
|
+
Comparators.compareBy((it) => Signatures.sign(mapper(it)), Comparators.natural())
|
100
|
+
)
|
101
|
+
}
|
102
|
+
|
103
|
+
export const sort = <T extends Signable>(array: Array<T>): Array<T> => sortBy(array, Signatures.sign)
|
104
|
+
|
105
|
+
export const concatenate = concat
|
106
|
+
|
107
|
+
export const first = _first
|
108
|
+
|
109
|
+
export const only = <T>(array: Array<T>): T => {
|
110
|
+
Preconditions.isTrue(array.length === 1)
|
111
|
+
return first(array)!
|
112
|
+
}
|
113
|
+
|
114
|
+
export const last = _last
|
115
|
+
|
116
|
+
export const isEmpty = _isEmpty
|
117
|
+
// TODO make a better range function
|
118
|
+
export const range = _range
|
119
|
+
// TODO should this live in collections?
|
120
|
+
export const groupBy = _groupBy
|
121
|
+
|
122
|
+
export const rest = <T>(array: Array<T>, elementsToSkip: number = 1): Array<T> => {
|
123
|
+
return array.slice(elementsToSkip)
|
124
|
+
}
|
125
|
+
|
126
|
+
export const clear = (array: Array<unknown>): void => {
|
127
|
+
_remove(array, () => true)
|
128
|
+
}
|
129
|
+
|
130
|
+
export const bisect = <T, L, R>(array: Array<T>, bisector: (element: T, index: number) => Either<L, R>): [Array<L>, Array<R>] => {
|
131
|
+
return Eithers.split(array.map(bisector))
|
132
|
+
}
|
133
|
+
|
134
|
+
export const toArray = <T>(array: Arrayable<T>): Array<T> => {
|
135
|
+
if (Array.isArray(array)) {
|
136
|
+
return array
|
137
|
+
}
|
138
|
+
|
139
|
+
return [array]
|
140
|
+
}
|
141
|
+
|
142
|
+
export const flatten = _flatten
|
package/src/async.ts
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
import { Duration } from '@bessemer/cornerstone/duration'
|
2
|
+
import { Durations, Objects } from '@bessemer/cornerstone'
|
3
|
+
|
4
|
+
export type PendingValue = {
|
5
|
+
isSuccess: false
|
6
|
+
isError: false
|
7
|
+
isLoading: false
|
8
|
+
isFetching: boolean
|
9
|
+
data: undefined
|
10
|
+
}
|
11
|
+
|
12
|
+
export type LoadingValue = {
|
13
|
+
isSuccess: false
|
14
|
+
isError: false
|
15
|
+
isLoading: true
|
16
|
+
isFetching: boolean
|
17
|
+
data: undefined
|
18
|
+
}
|
19
|
+
|
20
|
+
export type ErrorValue = {
|
21
|
+
isSuccess: false
|
22
|
+
isError: true
|
23
|
+
isLoading: false
|
24
|
+
isFetching: boolean
|
25
|
+
data: unknown
|
26
|
+
}
|
27
|
+
|
28
|
+
export type FetchingValueSuccess<T> = {
|
29
|
+
isSuccess: true
|
30
|
+
isError: false
|
31
|
+
isLoading: false
|
32
|
+
isFetching: true
|
33
|
+
data: T
|
34
|
+
}
|
35
|
+
|
36
|
+
export type FetchingValueError = {
|
37
|
+
isSuccess: false
|
38
|
+
isError: true
|
39
|
+
isLoading: false
|
40
|
+
isFetching: true
|
41
|
+
data: unknown
|
42
|
+
}
|
43
|
+
|
44
|
+
export type SettledValue<T> = {
|
45
|
+
isSuccess: true
|
46
|
+
isError: false
|
47
|
+
isLoading: false
|
48
|
+
isFetching: false
|
49
|
+
data: T
|
50
|
+
}
|
51
|
+
|
52
|
+
export type AsyncValue<T> = PendingValue | LoadingValue | ErrorValue | FetchingValueSuccess<T> | FetchingValueError | SettledValue<T>
|
53
|
+
|
54
|
+
export const isSettled = <T>(value: AsyncValue<T>): value is SettledValue<T> => {
|
55
|
+
return value.isSuccess && !value.isError && !value.isLoading && !value.isFetching
|
56
|
+
}
|
57
|
+
|
58
|
+
export const loading = (): LoadingValue => ({ isSuccess: false, isError: false, isLoading: true, isFetching: true, data: undefined })
|
59
|
+
|
60
|
+
export const fetching = <T>(data: T): FetchingValueSuccess<T> => ({
|
61
|
+
isSuccess: true,
|
62
|
+
isError: false,
|
63
|
+
isLoading: false,
|
64
|
+
isFetching: true,
|
65
|
+
data,
|
66
|
+
})
|
67
|
+
|
68
|
+
export const settled = <T>(data: T): SettledValue<T> => ({ isSuccess: true, isError: false, isLoading: false, isFetching: false, data })
|
69
|
+
|
70
|
+
export const error = (error: unknown): ErrorValue => ({ isSuccess: false, isError: true, isLoading: false, isFetching: false, data: error })
|
71
|
+
|
72
|
+
export const handle = <T, N>(
|
73
|
+
value: AsyncValue<T | null>,
|
74
|
+
handlers: { loading: () => N; error: (error: unknown) => N; absent: () => N; success: (data: T) => N }
|
75
|
+
): N => {
|
76
|
+
if (value.isLoading || (value.isError && value.isFetching)) {
|
77
|
+
return handlers.loading()
|
78
|
+
}
|
79
|
+
if (value.isError) {
|
80
|
+
return handlers.error(value.data)
|
81
|
+
}
|
82
|
+
if (Objects.isNil(value.data)) {
|
83
|
+
return handlers.absent()
|
84
|
+
}
|
85
|
+
|
86
|
+
return handlers.success(value.data)
|
87
|
+
}
|
88
|
+
|
89
|
+
export const map = <T, N>(value: AsyncValue<T>, mapper: (value: T) => N): AsyncValue<N> => {
|
90
|
+
if (!value.isSuccess) {
|
91
|
+
return value
|
92
|
+
}
|
93
|
+
|
94
|
+
return { ...value, data: mapper(value.data) }
|
95
|
+
}
|
96
|
+
|
97
|
+
export const execute = <T>(runnable: () => Promise<T>): Promise<T> => {
|
98
|
+
return new Promise(async (resolve, reject) => {
|
99
|
+
setTimeout(async () => {
|
100
|
+
try {
|
101
|
+
const value = await runnable()
|
102
|
+
resolve(value)
|
103
|
+
} catch (e) {
|
104
|
+
reject(e)
|
105
|
+
}
|
106
|
+
}, 0)
|
107
|
+
})
|
108
|
+
}
|
109
|
+
|
110
|
+
export const sleep = (duration: Duration): Promise<void> => {
|
111
|
+
return new Promise((resolve) => {
|
112
|
+
setTimeout(resolve, Durations.inMilliseconds(duration))
|
113
|
+
})
|
114
|
+
}
|
package/src/cache.ts
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
import { AbstractLocalKeyValueStore, AbstractRemoteKeyValueStore, LocalKeyValueStore, RemoteKeyValueStore } from '@bessemer/cornerstone/store'
|
2
|
+
import { Arrays, Dates, Durations, Objects, Strings, Zod } from '@bessemer/cornerstone'
|
3
|
+
import { Duration } from '@bessemer/cornerstone/duration'
|
4
|
+
import { ResourceKey, ResourceNamespace } from '@bessemer/cornerstone/resource'
|
5
|
+
import { AbstractApplicationContext } from '@bessemer/cornerstone/context'
|
6
|
+
import { NominalType } from '@bessemer/cornerstone/types'
|
7
|
+
import { Entry } from '@bessemer/cornerstone/entry'
|
8
|
+
import { GlobPattern } from '@bessemer/cornerstone/glob'
|
9
|
+
import { Arrayable } from 'type-fest'
|
10
|
+
import { ZodType } from 'zod'
|
11
|
+
|
12
|
+
// JOHN should this even be in cornerstone? especially consider the config types down at the bottom
|
13
|
+
|
14
|
+
export type CacheProps = {
|
15
|
+
maxSize: number | null
|
16
|
+
timeToLive: Duration
|
17
|
+
timeToStale: Duration | null
|
18
|
+
}
|
19
|
+
|
20
|
+
export type CacheOptions = Partial<CacheProps>
|
21
|
+
|
22
|
+
export namespace CacheProps {
|
23
|
+
const DefaultCacheProps = {
|
24
|
+
maxSize: 50000,
|
25
|
+
timeToLive: Durations.OneDay,
|
26
|
+
timeToStale: Durations.OneHour,
|
27
|
+
}
|
28
|
+
|
29
|
+
export const buildCacheProps = (options?: CacheOptions): CacheProps => {
|
30
|
+
options = options ?? {}
|
31
|
+
|
32
|
+
const props = Objects.merge(DefaultCacheProps, options)
|
33
|
+
|
34
|
+
if (props.maxSize === null && props.timeToLive === null) {
|
35
|
+
throw new Error('Invalid cache configuration, both maxSize and timeToLive are null')
|
36
|
+
}
|
37
|
+
|
38
|
+
return props
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
export namespace CacheKey {
|
43
|
+
// We use a hardcoded UUID to represent a unique token value that serves as a flag to disable caching
|
44
|
+
const DisableCacheToken = 'f6822c1a-d527-4c65-b9dd-ddc24620b684'
|
45
|
+
|
46
|
+
export const disableCaching = (): ResourceNamespace => {
|
47
|
+
return DisableCacheToken
|
48
|
+
}
|
49
|
+
|
50
|
+
export const isDisabled = (key: ResourceNamespace): boolean => {
|
51
|
+
return Strings.contains(key, DisableCacheToken)
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
export type CacheSector = {
|
56
|
+
globs: Array<GlobPattern>
|
57
|
+
}
|
58
|
+
|
59
|
+
export namespace CacheSector {
|
60
|
+
export const of = (globs: Arrayable<GlobPattern>) => {
|
61
|
+
return { globs: Arrays.toArray(globs) }
|
62
|
+
}
|
63
|
+
|
64
|
+
export const namespace = (namespace: ResourceNamespace, sector: CacheSector): CacheSector => {
|
65
|
+
return { globs: ResourceKey.namespaceAll(namespace, sector.globs) }
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
export type CacheName = NominalType<string, 'CacheName'>
|
70
|
+
export const CacheNameSchema: ZodType<CacheName> = Zod.string()
|
71
|
+
|
72
|
+
export interface AbstractCache<T> {
|
73
|
+
name: CacheName
|
74
|
+
}
|
75
|
+
|
76
|
+
export interface Cache<T> extends AbstractCache<T> {
|
77
|
+
fetchValue(namespace: ResourceNamespace, key: ResourceKey, fetch: () => Promise<T>): Promise<T>
|
78
|
+
|
79
|
+
fetchValues(
|
80
|
+
namespace: ResourceNamespace,
|
81
|
+
keys: Array<ResourceKey>,
|
82
|
+
fetch: (keys: Array<ResourceKey>) => Promise<Array<Entry<T>>>
|
83
|
+
): Promise<Array<Entry<T>>>
|
84
|
+
|
85
|
+
writeValue(namespace: ResourceNamespace, key: ResourceKey, value: T | undefined): Promise<void>
|
86
|
+
|
87
|
+
writeValues(namespace: ResourceNamespace, entries: Array<Entry<T | undefined>>): Promise<void>
|
88
|
+
|
89
|
+
evictAll(sector: CacheSector): Promise<void>
|
90
|
+
}
|
91
|
+
|
92
|
+
export interface CacheProvider<T> extends RemoteKeyValueStore<CacheEntry<T>> {
|
93
|
+
type: CacheProviderType
|
94
|
+
|
95
|
+
evictAll(sector: CacheSector): Promise<void>
|
96
|
+
}
|
97
|
+
|
98
|
+
export abstract class AbstractCacheProvider<T> extends AbstractRemoteKeyValueStore<CacheEntry<T>> implements CacheProvider<T> {
|
99
|
+
abstract type: CacheProviderType
|
100
|
+
|
101
|
+
abstract evictAll(sector: CacheSector): Promise<void>
|
102
|
+
}
|
103
|
+
|
104
|
+
export interface LocalCache<T> extends AbstractCache<T> {
|
105
|
+
getValue(namespace: ResourceNamespace, key: ResourceKey, fetch: () => T): T
|
106
|
+
|
107
|
+
getValues(namespace: ResourceNamespace, keys: Array<ResourceKey>, fetch: (keys: Array<ResourceKey>) => Array<Entry<T>>): Array<Entry<T>>
|
108
|
+
|
109
|
+
setValue(namespace: ResourceNamespace, key: ResourceKey, value: T | undefined): void
|
110
|
+
|
111
|
+
setValues(namespace: ResourceNamespace, entries: Array<Entry<T | undefined>>): void
|
112
|
+
|
113
|
+
removeAll(sector: CacheSector): void
|
114
|
+
}
|
115
|
+
|
116
|
+
export interface LocalCacheProvider<T> extends LocalKeyValueStore<CacheEntry<T>>, CacheProvider<T> {
|
117
|
+
removeAll(sector: CacheSector): void
|
118
|
+
}
|
119
|
+
|
120
|
+
export abstract class AbstractLocalCacheProvider<T> extends AbstractLocalKeyValueStore<CacheEntry<T>> implements LocalCacheProvider<T> {
|
121
|
+
abstract type: CacheProviderType
|
122
|
+
|
123
|
+
abstract removeAll(sector: CacheSector): void
|
124
|
+
|
125
|
+
async evictAll(sector: CacheSector): Promise<void> {
|
126
|
+
this.removeAll(sector)
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
export type CacheEntry<T> = {
|
131
|
+
value: T
|
132
|
+
liveTimestamp: Date | null
|
133
|
+
staleTimestamp: Date | null
|
134
|
+
}
|
135
|
+
|
136
|
+
export namespace CacheEntry {
|
137
|
+
export const isActive = <T>(entry: CacheEntry<T>): boolean => {
|
138
|
+
if (isDead(entry) || isStale(entry)) {
|
139
|
+
return false
|
140
|
+
}
|
141
|
+
|
142
|
+
return true
|
143
|
+
}
|
144
|
+
|
145
|
+
export const isDead = <T>(entry: CacheEntry<T> | undefined): boolean => {
|
146
|
+
if (Objects.isNil(entry)) {
|
147
|
+
return true
|
148
|
+
}
|
149
|
+
|
150
|
+
if (Objects.isNil(entry.liveTimestamp)) {
|
151
|
+
return false
|
152
|
+
}
|
153
|
+
|
154
|
+
return Dates.isBefore(entry.liveTimestamp, Dates.now())
|
155
|
+
}
|
156
|
+
|
157
|
+
export const isAlive = <T>(entry: CacheEntry<T> | undefined): boolean => !isDead(entry)
|
158
|
+
|
159
|
+
export const isStale = <T>(entry: CacheEntry<T>): boolean => {
|
160
|
+
if (Objects.isNil(entry.staleTimestamp)) {
|
161
|
+
return false
|
162
|
+
}
|
163
|
+
|
164
|
+
return Dates.isBefore(entry.staleTimestamp, Dates.now())
|
165
|
+
}
|
166
|
+
|
167
|
+
export const of = <T>(value: T) => {
|
168
|
+
const entry: CacheEntry<T> = {
|
169
|
+
value,
|
170
|
+
liveTimestamp: null,
|
171
|
+
staleTimestamp: null,
|
172
|
+
}
|
173
|
+
|
174
|
+
return entry
|
175
|
+
}
|
176
|
+
|
177
|
+
// JOHN do we want to enforce some kind of minimum liveness threshold?
|
178
|
+
export const applyProps = <T>(originalEntry: CacheEntry<T>, props: CacheProps): CacheEntry<T> => {
|
179
|
+
let liveTimestamp: Date | null = originalEntry.liveTimestamp
|
180
|
+
if (!Objects.isNil(props.timeToLive)) {
|
181
|
+
const limit = Dates.addMilliseconds(Dates.now(), Durations.inMilliseconds(props.timeToLive))
|
182
|
+
if (Dates.isBefore(limit, liveTimestamp ?? limit)) {
|
183
|
+
liveTimestamp = limit
|
184
|
+
}
|
185
|
+
}
|
186
|
+
|
187
|
+
let staleTimestamp: Date | null = originalEntry.staleTimestamp
|
188
|
+
if (!Objects.isNil(props.timeToStale)) {
|
189
|
+
const limit = Dates.addMilliseconds(Dates.now(), Durations.inMilliseconds(props.timeToStale))
|
190
|
+
if (Dates.isBefore(limit, staleTimestamp ?? limit)) {
|
191
|
+
staleTimestamp = limit
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
const limitedEntry: CacheEntry<T> = {
|
196
|
+
value: originalEntry.value,
|
197
|
+
liveTimestamp,
|
198
|
+
staleTimestamp,
|
199
|
+
}
|
200
|
+
|
201
|
+
return limitedEntry
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
export type CacheConfigurationOptions = CacheConfigurationSection & {
|
206
|
+
local: CacheConfigurationSection
|
207
|
+
}
|
208
|
+
|
209
|
+
export type CacheConfigurationSection = {
|
210
|
+
defaults: CacheDefinition
|
211
|
+
|
212
|
+
/**
|
213
|
+
* These options map from cache name key to configuration. They are a way for the tenant to override the configurations
|
214
|
+
* for specific caches from the cache configuration here.
|
215
|
+
*/
|
216
|
+
caches?: Record<string, Partial<CacheDefinition>>
|
217
|
+
}
|
218
|
+
|
219
|
+
export type CacheDefinition = {
|
220
|
+
options?: CacheOptions
|
221
|
+
providers: Array<CacheProviderConfiguration>
|
222
|
+
}
|
223
|
+
|
224
|
+
export type CacheProviderType = NominalType<string, 'CacheProviderType'>
|
225
|
+
export type CacheProviderConfiguration = CacheOptions & {
|
226
|
+
type: CacheProviderType
|
227
|
+
}
|
228
|
+
|
229
|
+
export type CacheConfiguration = CacheConfigurationSection & {
|
230
|
+
local: CacheConfigurationSection
|
231
|
+
}
|
232
|
+
|
233
|
+
export type CacheProviderRegistry<ContextType extends AbstractApplicationContext> = {
|
234
|
+
type: CacheProviderType
|
235
|
+
construct: <T>(props: CacheProps, context: ContextType) => CacheProvider<T>
|
236
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import { Arrays, Sets } from '@bessemer/cornerstone'
|
2
|
+
|
3
|
+
export interface Combinable {
|
4
|
+
combinability: Combinability
|
5
|
+
}
|
6
|
+
|
7
|
+
export type Combinability = {
|
8
|
+
type: CombinabilityType
|
9
|
+
class: CombinabilityClass
|
10
|
+
}
|
11
|
+
|
12
|
+
export enum CombinabilityType {
|
13
|
+
Stackable = 'Stackable',
|
14
|
+
Singleton = 'Singleton',
|
15
|
+
Totalitarian = 'Totalitarian',
|
16
|
+
}
|
17
|
+
|
18
|
+
export type CombinabilityClass = string | null
|
19
|
+
|
20
|
+
export const DefaultCombinability: Combinability = {
|
21
|
+
type: CombinabilityType.Stackable,
|
22
|
+
class: null,
|
23
|
+
}
|
24
|
+
|
25
|
+
export const combinations = <T extends Combinable>(combinables: Array<T>): Array<Array<T>> => {
|
26
|
+
const classMap = Arrays.groupBy(combinables, (it) => it.combinability.class)
|
27
|
+
|
28
|
+
const classCombinations: Array<Array<Array<T>>> = Object.entries(classMap).map(([_, values]) => {
|
29
|
+
const totalitarianCombinations = values.filter((it) => it.combinability.type === CombinabilityType.Totalitarian).map((it) => [it])
|
30
|
+
if (!Arrays.isEmpty(totalitarianCombinations)) {
|
31
|
+
return totalitarianCombinations
|
32
|
+
}
|
33
|
+
|
34
|
+
const singletonCombinations = values.filter((it) => it.combinability.type === CombinabilityType.Singleton).map((it) => [it])
|
35
|
+
const stackableCombination = values.filter((it) => it.combinability.type === CombinabilityType.Stackable)
|
36
|
+
return [stackableCombination, ...singletonCombinations]
|
37
|
+
})
|
38
|
+
|
39
|
+
return Sets.cartesianProduct(...classCombinations).flatMap((it) => it)
|
40
|
+
}
|
@@ -0,0 +1,78 @@
|
|
1
|
+
import { Maths, Strings } from '@bessemer/cornerstone'
|
2
|
+
|
3
|
+
export type Comparator<T> = (first: T, second: T) => number
|
4
|
+
|
5
|
+
export const aggregate = <T>(comparators: Array<Comparator<T>>): Comparator<T> => {
|
6
|
+
return (first, second) => {
|
7
|
+
if (first === second) {
|
8
|
+
return 0
|
9
|
+
}
|
10
|
+
|
11
|
+
for (const comparator of comparators) {
|
12
|
+
const result = comparator(first, second)
|
13
|
+
if (result !== 0) {
|
14
|
+
return result
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
return 0
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
export const compareBy = <T, N>(mapper: (element: T) => N, comparator: Comparator<N>): Comparator<T> => {
|
23
|
+
return (first, second) => comparator(mapper(first), mapper(second))
|
24
|
+
}
|
25
|
+
|
26
|
+
export const reverse = <T>(comparator: Comparator<T>): Comparator<T> => {
|
27
|
+
return (first, second) => -comparator(first, second)
|
28
|
+
}
|
29
|
+
|
30
|
+
export const trueFirst = (): Comparator<boolean> => {
|
31
|
+
return (first, second) => natural()(first ? 1 : 0, second ? 1 : 0)
|
32
|
+
}
|
33
|
+
|
34
|
+
export const natural = (): Comparator<string | number | null> => {
|
35
|
+
// Comparing by nulls first allows us to assume the elements are non-null for future comparisons
|
36
|
+
return aggregate([
|
37
|
+
nullsLast(),
|
38
|
+
(first, second) => {
|
39
|
+
if (Strings.isString(first) && Strings.isString(second)) {
|
40
|
+
return first.localeCompare(second)
|
41
|
+
} else if (Maths.isNumber(first) && Maths.isNumber(second)) {
|
42
|
+
return first! - second!
|
43
|
+
} else if (Maths.isNumber(first)) {
|
44
|
+
return -1
|
45
|
+
} else {
|
46
|
+
return 1
|
47
|
+
}
|
48
|
+
},
|
49
|
+
])
|
50
|
+
}
|
51
|
+
|
52
|
+
export function matchedFirst<T>(target: T): Comparator<T | null> {
|
53
|
+
return aggregate([
|
54
|
+
nullsLast(),
|
55
|
+
(first, second) => {
|
56
|
+
if (first === target && second !== target) {
|
57
|
+
return -1
|
58
|
+
} else if (first !== target && second === target) {
|
59
|
+
return 1
|
60
|
+
} else {
|
61
|
+
return 0
|
62
|
+
}
|
63
|
+
},
|
64
|
+
])
|
65
|
+
}
|
66
|
+
|
67
|
+
export const nullsLast = <T>(): Comparator<T> => {
|
68
|
+
return (first, second) => {
|
69
|
+
if (first === null) {
|
70
|
+
return 1
|
71
|
+
}
|
72
|
+
if (second === null) {
|
73
|
+
return -1
|
74
|
+
}
|
75
|
+
|
76
|
+
return 0
|
77
|
+
}
|
78
|
+
}
|