@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.
Files changed (66) hide show
  1. package/jest.config.js +3 -0
  2. package/package.json +39 -0
  3. package/src/array.ts +142 -0
  4. package/src/async.ts +114 -0
  5. package/src/cache.ts +236 -0
  6. package/src/combinable.ts +40 -0
  7. package/src/comparator.ts +78 -0
  8. package/src/content.ts +138 -0
  9. package/src/context.ts +6 -0
  10. package/src/crypto.ts +11 -0
  11. package/src/date.ts +18 -0
  12. package/src/duration.ts +57 -0
  13. package/src/either.ts +29 -0
  14. package/src/entry.ts +21 -0
  15. package/src/equalitor.ts +12 -0
  16. package/src/error-event.ts +126 -0
  17. package/src/error.ts +16 -0
  18. package/src/expression/array-expression.ts +29 -0
  19. package/src/expression/expression-evaluator.ts +34 -0
  20. package/src/expression/expression.ts +188 -0
  21. package/src/expression/internal.ts +34 -0
  22. package/src/expression/numeric-expression.ts +182 -0
  23. package/src/expression/string-expression.ts +38 -0
  24. package/src/expression.ts +48 -0
  25. package/src/function.ts +3 -0
  26. package/src/glob.ts +19 -0
  27. package/src/global-variable.ts +40 -0
  28. package/src/hash.ts +28 -0
  29. package/src/hex-code.ts +6 -0
  30. package/src/index.ts +82 -0
  31. package/src/lazy.ts +11 -0
  32. package/src/logger.ts +144 -0
  33. package/src/math.ts +132 -0
  34. package/src/misc.ts +22 -0
  35. package/src/object.ts +236 -0
  36. package/src/patch.ts +128 -0
  37. package/src/precondition.ts +25 -0
  38. package/src/promise.ts +16 -0
  39. package/src/property.ts +29 -0
  40. package/src/reference.ts +68 -0
  41. package/src/resource.ts +32 -0
  42. package/src/result.ts +66 -0
  43. package/src/retry.ts +70 -0
  44. package/src/rich-text.ts +24 -0
  45. package/src/set.ts +46 -0
  46. package/src/signature.ts +20 -0
  47. package/src/store.ts +91 -0
  48. package/src/string.ts +173 -0
  49. package/src/tag.ts +68 -0
  50. package/src/types.ts +21 -0
  51. package/src/ulid.ts +28 -0
  52. package/src/unit.ts +4 -0
  53. package/src/uri.ts +321 -0
  54. package/src/url.ts +155 -0
  55. package/src/uuid.ts +37 -0
  56. package/src/zod.ts +24 -0
  57. package/test/comparator.test.ts +1 -0
  58. package/test/expression.test.ts +12 -0
  59. package/test/object.test.ts +104 -0
  60. package/test/patch.test.ts +170 -0
  61. package/test/set.test.ts +20 -0
  62. package/test/string.test.ts +22 -0
  63. package/test/uri.test.ts +111 -0
  64. package/test/url.test.ts +174 -0
  65. package/tsconfig.build.json +13 -0
  66. package/tsup.config.ts +4 -0
package/jest.config.js ADDED
@@ -0,0 +1,3 @@
1
+ import BaseConfig from '../jest.base.mjs'
2
+
3
+ export default BaseConfig
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
+ }