@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/src/logger.ts ADDED
@@ -0,0 +1,144 @@
1
+ import pino from 'pino'
2
+ import { Lazy, Objects } from '@bessemer/cornerstone'
3
+ import { createGlobalVariable } from '@bessemer/cornerstone/global-variable'
4
+ import { LazyValue } from '@bessemer/cornerstone/lazy'
5
+ import { UnknownRecord } from 'type-fest'
6
+
7
+ type PinoLogger = pino.Logger
8
+ type PinoBindings = pino.Bindings
9
+ export type LoggerOptions = pino.LoggerOptions
10
+
11
+ type LogOptions = { error?: unknown; context?: UnknownRecord }
12
+ type LogFunction = (message: LazyValue<string>, options?: LogOptions) => void
13
+
14
+ export class Logger {
15
+ constructor(private readonly logger: PinoLogger) {}
16
+
17
+ trace: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
18
+ if (this.logger.isLevelEnabled?.('trace') ?? true) {
19
+ this.logger.trace({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
20
+ }
21
+ }
22
+
23
+ debug: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
24
+ if (this.logger.isLevelEnabled?.('debug') ?? true) {
25
+ this.logger.debug({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
26
+ }
27
+ }
28
+
29
+ info: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
30
+ if (this.logger.isLevelEnabled?.('info') ?? true) {
31
+ this.logger.info({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
32
+ }
33
+ }
34
+
35
+ warn: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
36
+ if (this.logger.isLevelEnabled?.('warn') ?? true) {
37
+ this.logger.warn({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
38
+ }
39
+ }
40
+
41
+ error: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
42
+ if (this.logger.isLevelEnabled?.('error') ?? true) {
43
+ this.logger.error({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
44
+ }
45
+ }
46
+
47
+ fatal: LogFunction = (message: LazyValue<string>, options?: LogOptions): void => {
48
+ if (this.logger.isLevelEnabled?.('fatal') ?? true) {
49
+ this.logger.fatal({ err: options?.error, context: options?.context }, Lazy.evaluate(message))
50
+ }
51
+ }
52
+ }
53
+
54
+ const getPrettyTransport = (): LoggerOptions => {
55
+ if (process.env.NODE_ENV === 'production' || typeof window !== 'undefined') {
56
+ return {}
57
+ }
58
+
59
+ return {
60
+ transport: {
61
+ target: 'pino-pretty',
62
+ options: {
63
+ colorize: true,
64
+ ignore: 'pid,hostname,module',
65
+ messageFormat: '{if module}{module} - {end}{msg}',
66
+ },
67
+ },
68
+ }
69
+ }
70
+
71
+ const applyDefaultOptions = (options?: LoggerOptions): LoggerOptions => {
72
+ const defaultOptions: LoggerOptions = {
73
+ browser: {
74
+ asObject: true,
75
+ },
76
+ ...getPrettyTransport(),
77
+ }
78
+
79
+ return Objects.merge(defaultOptions, options)
80
+ }
81
+
82
+ const createProxyHandler = (getLogger: () => PinoLogger): ProxyHandler<PinoLogger> => {
83
+ let cachedLogger: PinoLogger | null = null
84
+ let cachedVersion = GlobalLoggerState.getValue().version
85
+
86
+ const getOrCreateLogger = () => {
87
+ if (cachedVersion !== GlobalLoggerState.getValue().version) {
88
+ cachedLogger = null
89
+ cachedVersion = GlobalLoggerState.getValue().version
90
+ }
91
+
92
+ if (Objects.isNil(cachedLogger)) {
93
+ cachedLogger = getLogger()
94
+ }
95
+
96
+ return cachedLogger
97
+ }
98
+
99
+ return {
100
+ get(_: any, prop: string): any {
101
+ if (prop === 'child') {
102
+ return (bindings: PinoBindings) => {
103
+ return new Proxy(
104
+ {} as PinoLogger,
105
+ createProxyHandler(() => getOrCreateLogger().child(bindings))
106
+ )
107
+ }
108
+ }
109
+
110
+ return (getOrCreateLogger() as any)[prop]
111
+ },
112
+ }
113
+ }
114
+
115
+ const GlobalLoggerState = createGlobalVariable<{
116
+ version: number
117
+ logger: pino.Logger
118
+ }>('GlobalLoggerState', () => ({
119
+ version: 0,
120
+ logger: pino(applyDefaultOptions({ level: 'info' })),
121
+ }))
122
+
123
+ const LoggerProxy: PinoLogger = new Proxy(
124
+ {} as PinoLogger,
125
+ createProxyHandler(() => GlobalLoggerState.getValue().logger)
126
+ )
127
+
128
+ const Primary: Logger = new Logger(LoggerProxy)
129
+
130
+ export const initialize = (initialOptions?: LoggerOptions): void => {
131
+ const options = applyDefaultOptions(initialOptions)
132
+ GlobalLoggerState.setValue({ version: GlobalLoggerState.getValue().version + 1, logger: pino(options) })
133
+ }
134
+
135
+ export const child = (module: string): Logger => {
136
+ return new Logger(LoggerProxy.child({ module }))
137
+ }
138
+
139
+ export const trace = Primary.trace
140
+ export const debug = Primary.debug
141
+ export const info = Primary.info
142
+ export const warn = Primary.warn
143
+ export const error = Primary.error
144
+ export const fatal = Primary.fatal
package/src/math.ts ADDED
@@ -0,0 +1,132 @@
1
+ import { isNumber as _isNumber } from 'lodash-es'
2
+
3
+ export const isNumber = (value?: unknown): value is number => {
4
+ return _isNumber(value)
5
+ }
6
+
7
+ export const isPositive = (value?: unknown): value is number => {
8
+ return isNumber(value) && value > 0
9
+ }
10
+
11
+ export const isEven = (d: number) => d % 2 === 0
12
+
13
+ export enum RoundingMode {
14
+ Nearest = 'Nearest',
15
+ Down = 'Down',
16
+ Up = 'Up',
17
+ HalfEven = 'HalfEven',
18
+ }
19
+
20
+ export const round = (value: number, scale: number, roundingMode: RoundingMode): number => {
21
+ switch (roundingMode) {
22
+ case RoundingMode.Nearest:
23
+ return roundNearest(value, scale)
24
+ case RoundingMode.Down:
25
+ return roundDown(value, scale)
26
+ case RoundingMode.Up:
27
+ return roundUp(value, scale)
28
+ case RoundingMode.HalfEven:
29
+ return roundHalfEven(value, scale)
30
+ }
31
+ }
32
+
33
+ export const roundNearest = (value: number, scale: number): number => {
34
+ const factor = Math.pow(10, scale)
35
+ return Math.round((value + Number.EPSILON) * factor) / factor
36
+ }
37
+
38
+ export const roundDown = (value: number, scale: number) => {
39
+ const factor = Math.pow(10, scale)
40
+ return Math.floor((value + +Number.EPSILON) * factor) / factor
41
+ }
42
+
43
+ export const roundUp = (value: number, scale: number) => {
44
+ const factor = Math.pow(10, scale)
45
+ return Math.ceil((value + +Number.EPSILON) * factor) / factor
46
+ }
47
+
48
+ /**
49
+ * Round Half-Even (Banker's Rounding) Utility
50
+ * https://en.wikipedia.org/wiki/Rounding#Round_half_to_even
51
+ *
52
+ * Mostly copied from this Github: https://github.com/schowdhuri/round-half-even
53
+ */
54
+ export const roundHalfEven = (value: number, scale: number): number => {
55
+ if (value < 0) {
56
+ return -roundHalfEven(-value, scale)
57
+ }
58
+ if (scale === 0) {
59
+ return roundHalfEven(value / 10, 1) * 10
60
+ }
61
+
62
+ const MAX_DECIMALS_ALLOWED = 20
63
+ if (scale > MAX_DECIMALS_ALLOWED) {
64
+ throw new Error(`Cannot handle more than ${MAX_DECIMALS_ALLOWED} decimals`)
65
+ }
66
+
67
+ // convert to string; remove trailing 0s
68
+ const isExponentialForm = value.toString().includes('e') || value.toString().includes('E')
69
+ const strNum = (isExponentialForm ? value.toFixed(MAX_DECIMALS_ALLOWED).toString() : value.toString()).replace(/0+$/, '')
70
+ const decimalIndex = strNum.indexOf('.')
71
+ if (decimalIndex < 0) {
72
+ // no fractional part
73
+ return value
74
+ }
75
+ let intPart: string = strNum.slice(0, decimalIndex)
76
+ if (intPart.length == 0) {
77
+ intPart = '0'
78
+ }
79
+ let fractPart = strNum.slice(decimalIndex + 1) // extract fractional part
80
+ if (fractPart.length < scale) {
81
+ return value
82
+ }
83
+ const followingDig = parseInt(fractPart[scale]!, 10)
84
+ if (followingDig < 5) {
85
+ // rounding not required
86
+ const newFractPart = fractPart.slice(0, scale)
87
+ return parseFloat(`${intPart}.${newFractPart}`)
88
+ }
89
+ if (followingDig === 5) {
90
+ const newFractPart = fractPart.slice(0, scale + 1)
91
+ if (parseInt(fractPart.slice(scale + 1), 10) > 0) {
92
+ fractPart = `${newFractPart}9`
93
+ } else {
94
+ fractPart = newFractPart
95
+ }
96
+ }
97
+
98
+ let nextDig = parseInt(fractPart[fractPart.length - 1]!, 10)
99
+ let carriedOver = 0
100
+ for (let ptr = fractPart.length - 1; ptr >= scale; ptr--) {
101
+ let dig = parseInt(fractPart[ptr - 1]!, 10) + carriedOver
102
+ if (nextDig > 5 || (nextDig == 5 && !isEven(dig))) {
103
+ ++dig
104
+ }
105
+ if (dig > 9) {
106
+ dig -= 10
107
+ carriedOver = 1
108
+ } else {
109
+ carriedOver = 0
110
+ }
111
+ nextDig = dig
112
+ }
113
+
114
+ let newFractPart = ''
115
+ for (let ptr = scale - 2; ptr >= 0; ptr--) {
116
+ let d = parseInt(fractPart[ptr]!, 10) + carriedOver
117
+ if (d > 9) {
118
+ d -= 10
119
+ carriedOver = 1
120
+ } else {
121
+ carriedOver = 0
122
+ }
123
+ newFractPart = `${d}${newFractPart}`
124
+ }
125
+
126
+ const resolvedIntPart = parseInt(intPart, 10) + carriedOver
127
+ return parseFloat(`${resolvedIntPart}.${newFractPart}${nextDig}`)
128
+ }
129
+
130
+ export const random = (min: number, max: number): number => {
131
+ return Math.random() * (max - min) + min
132
+ }
package/src/misc.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { Objects, Preconditions } from '@bessemer/cornerstone'
2
+ import { Equalitor } from '@bessemer/cornerstone/equalitor'
3
+
4
+ export const doUntilConsistent = <T>(supplier: (previous: T | null) => T, equals: Equalitor<T>): T => {
5
+ let done = false
6
+ let previousValue: T | null = null
7
+ let attempts = 0
8
+ do {
9
+ Preconditions.isTrue(attempts < 10)
10
+
11
+ const currentValue = supplier(previousValue)
12
+
13
+ if (Objects.isPresent(previousValue) && equals(previousValue, currentValue)) {
14
+ done = true
15
+ }
16
+
17
+ previousValue = currentValue
18
+ attempts++
19
+ } while (!done)
20
+
21
+ return previousValue
22
+ }
package/src/object.ts ADDED
@@ -0,0 +1,236 @@
1
+ import {
2
+ clone as _clone,
3
+ cloneDeep as _cloneDeep,
4
+ invert as _invert,
5
+ isEqual as _isEqual,
6
+ isNil as _isNil,
7
+ isNumber,
8
+ isObject as _isObject,
9
+ isPlainObject as _isPlainObject,
10
+ isString,
11
+ isUndefined as _isUndefined,
12
+ mapValues as _mapValues,
13
+ merge as unsafeMerge,
14
+ mergeWith as unsafeMergeWith,
15
+ } from 'lodash-es'
16
+ import { produce } from 'immer'
17
+ import { NominalType } from '@bessemer/cornerstone/types'
18
+ import { Primitive, UnknownRecord } from 'type-fest'
19
+
20
+ export const update: typeof produce = produce
21
+
22
+ export const isUndefined = _isUndefined
23
+ export const isNil = _isNil
24
+ export const isPresent = <T>(value: T): value is NonNullable<T> => {
25
+ return !isNil(value)
26
+ }
27
+ export const isObject = _isObject
28
+ export const isPlainObject = _isPlainObject
29
+ export const deepEqual = _isEqual
30
+ export const invert = _invert
31
+ export const mapValues = _mapValues
32
+
33
+ export const clone = _clone
34
+ export const cloneDeep = _cloneDeep
35
+
36
+ export const mergeAll = <T>(objects: Array<T>): T => {
37
+ return objects.reduce((x, y) => merge(x, y))
38
+ }
39
+
40
+ export function merge<TObject, TSource>(object: TObject, source: TSource): TObject & TSource
41
+ export function merge<TObject, TSource1, TSource2>(object: TObject, source1: TSource1, source2: TSource2): TObject & TSource1 & TSource2
42
+ export function merge<TObject, TSource1, TSource2, TSource3>(
43
+ object: TObject,
44
+ source1: TSource1,
45
+ source2: TSource2,
46
+ source3: TSource3
47
+ ): TObject & TSource1 & TSource2 & TSource3
48
+ export function merge<TObject, TSource1, TSource2, TSource3, TSource4>(
49
+ object: TObject,
50
+ source1: TSource1,
51
+ source2: TSource2,
52
+ source3: TSource3,
53
+ source4: TSource4
54
+ ): TObject & TSource1 & TSource2 & TSource3 & TSource4
55
+ export function merge(object: any, ...otherArgs: any[]): any {
56
+ return unsafeMerge({}, object, ...otherArgs)
57
+ }
58
+
59
+ export function mergeInto<Source1, Source2>(source: Source1, values: Source2): asserts source is Source1 & Source2 {
60
+ unsafeMerge(source, values)
61
+ }
62
+
63
+ export const mergeWith: typeof unsafeMergeWith = (...args: Array<any>) => {
64
+ const clone = cloneDeep(args[0])
65
+ return unsafeMergeWith.apply(null, [clone, ...args.slice(1)])
66
+ }
67
+
68
+ export type ObjectDiffResult = {
69
+ elementsUpdated: Record<string, { originalValue: unknown; updatedValue: unknown }>
70
+ elementsAdded: UnknownRecord
71
+ elementsRemoved: UnknownRecord
72
+ }
73
+
74
+ export function diffShallow(original: UnknownRecord, updated: UnknownRecord): ObjectDiffResult {
75
+ const result: ObjectDiffResult = {
76
+ elementsUpdated: {},
77
+ elementsAdded: {},
78
+ elementsRemoved: {},
79
+ }
80
+
81
+ for (const [key, originalValue] of Object.entries(original)) {
82
+ const updatedValue = updated[key]
83
+ if (updatedValue === undefined) {
84
+ result.elementsRemoved[key] = originalValue
85
+ } else if (!deepEqual(originalValue, updatedValue)) {
86
+ result.elementsUpdated[key] = { originalValue: originalValue, updatedValue: updatedValue }
87
+ }
88
+ }
89
+
90
+ for (const [key, updatedValue] of Object.entries(updated)) {
91
+ const originalValue = original[key]
92
+ if (originalValue === undefined) {
93
+ result.elementsAdded[key] = updatedValue
94
+ }
95
+ }
96
+ return result
97
+ }
98
+
99
+ export const isValidKey = (field: PropertyKey, obj: object): field is keyof typeof obj => {
100
+ return field in obj
101
+ }
102
+
103
+ /** Determines if the list of fields are present on the object (not null or undefined), with type inference */
104
+ export function fieldsPresent<T extends object, K extends keyof T>(
105
+ object: T,
106
+ fields: Array<K>
107
+ ): object is Exclude<T, K> & Required<{ [P in K]: NonNullable<T[P]> }> {
108
+ return fields.every((field) => isPresent(object[field]))
109
+ }
110
+
111
+ export type ObjectPath = {
112
+ path: Array<string | number>
113
+ }
114
+
115
+ export const path = (path: Array<string | number>): ObjectPath => {
116
+ return { path }
117
+ }
118
+
119
+ export const parsePath = (path: string): ObjectPath => {
120
+ const result: Array<string | number> = []
121
+ const regex = /([^.\[\]]+)|\[(\d+)]/g
122
+
123
+ let match: RegExpExecArray | null
124
+ while ((match = regex.exec(path)) !== null) {
125
+ if (match[1] !== undefined) {
126
+ result.push(match[1])
127
+ } else if (match[2] !== undefined) {
128
+ result.push(Number(match[2]))
129
+ }
130
+ }
131
+
132
+ return { path: result }
133
+ }
134
+
135
+ const pathify = (path: ObjectPath | string): ObjectPath => {
136
+ if (isString(path)) {
137
+ return parsePath(path)
138
+ }
139
+
140
+ return path as ObjectPath
141
+ }
142
+
143
+ export const getPathValue = (object: UnknownRecord, initialPath: ObjectPath | string): unknown | undefined => {
144
+ const path = pathify(initialPath)
145
+ let current: any = object
146
+
147
+ for (const key of path.path) {
148
+ if (isPrimitive(current)) {
149
+ return undefined
150
+ }
151
+
152
+ current = current[key]
153
+ }
154
+
155
+ return current
156
+ }
157
+
158
+ export const applyPathValue = (object: UnknownRecord, initialPath: ObjectPath | string, value: unknown): UnknownRecord | undefined => {
159
+ const path = pathify(initialPath)
160
+
161
+ const newObject = update(object, (draft) => {
162
+ let current: any = draft
163
+
164
+ for (let i = 0; i < path.path.length; i++) {
165
+ const key = path.path[i]!
166
+ const isLastKey = i === path.path.length - 1
167
+
168
+ if (isPrimitive(current)) {
169
+ return
170
+ }
171
+
172
+ if (Array.isArray(current)) {
173
+ if (!isNumber(key)) {
174
+ return
175
+ }
176
+
177
+ if (key >= current.length) {
178
+ return
179
+ }
180
+ }
181
+
182
+ if (isLastKey) {
183
+ current[key] = value
184
+ } else {
185
+ current = current[key]
186
+ }
187
+ }
188
+ })
189
+
190
+ if (newObject === object) {
191
+ return undefined
192
+ }
193
+
194
+ return newObject
195
+ }
196
+
197
+ const isPrimitive = (value: any): value is Primitive => {
198
+ return value === null || (typeof value !== 'object' && typeof value !== 'function')
199
+ }
200
+
201
+ type TransformFunction = (value: any, path: (string | number)[], key: string | number, parent: any) => any
202
+
203
+ const walk = (value: any, transform: TransformFunction, path: (string | number)[] = []): any => {
204
+ if (isNil(value) || isPrimitive(value)) {
205
+ return value
206
+ }
207
+
208
+ if (Array.isArray(value)) {
209
+ return value.map((value, index) => {
210
+ const currentPath = [...path, index]
211
+ return walk(transform(value, currentPath, index, value), transform, currentPath)
212
+ })
213
+ }
214
+
215
+ const result: any = {}
216
+ for (const key in value) {
217
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
218
+ const currentPath = [...path, key]
219
+ const transformedValue = transform(value[key], currentPath, key, value)
220
+ result[key] = walk(transformedValue, transform, currentPath)
221
+ }
222
+ }
223
+
224
+ return result
225
+ }
226
+
227
+ export type RecordAttribute<Type = unknown, Class extends string = 'RecordAttribute'> = NominalType<string, [Type, Class]>
228
+ type RecordAttributeType<Attribute> = Attribute extends RecordAttribute<infer Type, string> ? Type : never
229
+
230
+ export const getAttribute = <T extends RecordAttribute<unknown, string>>(record: UnknownRecord, attribute: T): RecordAttributeType<T> | undefined => {
231
+ return record[attribute] as RecordAttributeType<T> | undefined
232
+ }
233
+
234
+ export const coerceNil = <T>(value: T | null | undefined): T | undefined => {
235
+ return isNil(value) ? undefined : value
236
+ }
package/src/patch.ts ADDED
@@ -0,0 +1,128 @@
1
+ import {
2
+ ArrayExpressions,
3
+ EvaluateExpression,
4
+ Expression,
5
+ Expressions,
6
+ NumericExpressions,
7
+ ReducingExpression,
8
+ } from '@bessemer/cornerstone/expression'
9
+ import { Objects, Preconditions } from '@bessemer/cornerstone'
10
+ import { UnknownRecord } from 'type-fest'
11
+
12
+ export enum PatchType {
13
+ Set = 'Set',
14
+ Apply = 'Apply',
15
+ Patch = 'Patch',
16
+ }
17
+
18
+ export type SetPatch<T> = {
19
+ _PatchType: PatchType.Set
20
+ value: Expression<T>
21
+ }
22
+
23
+ export type ApplyPatch<T> = {
24
+ _PatchType: PatchType.Apply
25
+ value: Expression<T>
26
+ reducer: ReducingExpression<T, T>
27
+ }
28
+
29
+ export type PatchPatch<T> = {
30
+ _PatchType: PatchType.Patch
31
+ patch: Patchable<T>
32
+ }
33
+
34
+ export type Patch<T> = SetPatch<T> | ApplyPatch<T> | PatchPatch<T>
35
+
36
+ export type PatchValue<T> = {
37
+ value: T
38
+ patch: Patch<T>
39
+ }
40
+
41
+ export type Patchable<T> = {
42
+ [P in keyof T]?: T[P] extends Array<infer U>
43
+ ? Patch<U[]> | Patchable<U[]>
44
+ : T[P] extends object | undefined
45
+ ? Patch<T[P]> | Patchable<T[P]>
46
+ : Patch<T[P]> | T[P]
47
+ }
48
+
49
+ export const set = <T>(value: Expression<T>): Patch<T> => {
50
+ return {
51
+ _PatchType: PatchType.Set,
52
+ value: value as any,
53
+ }
54
+ }
55
+
56
+ export const apply = <T>(value: Expression<T>, reducer: ReducingExpression<T, T>): Patch<T> => {
57
+ return {
58
+ _PatchType: PatchType.Apply,
59
+ value,
60
+ reducer,
61
+ }
62
+ }
63
+
64
+ export const patch = <T extends UnknownRecord, N extends Patchable<T> = Patchable<T>>(patch: N): Patch<T> => {
65
+ return {
66
+ _PatchType: PatchType.Patch,
67
+ patch,
68
+ }
69
+ }
70
+
71
+ export const sum = (value: Expression<number>): Patch<number> => {
72
+ return apply(value, Expressions.reference(NumericExpressions.SumExpression))
73
+ }
74
+
75
+ export const multiply = (value: Expression<number>): Patch<number> => {
76
+ return apply(value, Expressions.reference(NumericExpressions.MultiplyExpression))
77
+ }
78
+
79
+ export const concatenate = <T extends Array<Expression<unknown>>>(value: Expression<T>): Patch<T> => {
80
+ return apply(value, Expressions.reference(ArrayExpressions.ConcatenateExpression)) as Patch<T>
81
+ }
82
+
83
+ export type ResolvePatchesResult<T> = {
84
+ value: T
85
+ patchValues: Array<PatchValue<T>>
86
+ }
87
+
88
+ export const resolveWithDetails = <T>(value: T, patches: Array<Patch<T>>, evaluate: EvaluateExpression): ResolvePatchesResult<T> => {
89
+ let currentValue: T = value
90
+
91
+ const patchValues = patches.map((patch) => {
92
+ switch (patch._PatchType) {
93
+ case PatchType.Set:
94
+ currentValue = evaluate(patch.value)
95
+ break
96
+ case PatchType.Apply:
97
+ currentValue = evaluate(Expressions.dereference(patch.reducer, [currentValue, patch.value]))
98
+ break
99
+ case PatchType.Patch:
100
+ currentValue = applyPatch(currentValue, patch.patch, evaluate)
101
+ break
102
+ default:
103
+ Preconditions.isUnreachable(() => `Unrecognized PatchType for value: ${JSON.stringify(it)}`)
104
+ }
105
+
106
+ return { value: currentValue, patch }
107
+ })
108
+
109
+ return { value: currentValue, patchValues }
110
+ }
111
+
112
+ export const resolve = <T>(value: T, patches: Array<Patch<T>>, evaluate: EvaluateExpression): T => {
113
+ return resolveWithDetails(value, patches, evaluate).value
114
+ }
115
+
116
+ const applyPatch = <T>(value: T, patch: Patchable<T>, evaluate: EvaluateExpression): T => {
117
+ return Objects.mergeWith(value, patch, (value, patch) => {
118
+ if (Objects.isNil(patch)) {
119
+ return value
120
+ }
121
+
122
+ if (!Objects.isObject(patch) || !('_PatchType' in patch)) {
123
+ return undefined
124
+ }
125
+
126
+ return evaluate(resolve(value, [patch as Patch<T>], evaluate))
127
+ })
128
+ }
@@ -0,0 +1,25 @@
1
+ import { Lazy, Objects } from '@bessemer/cornerstone'
2
+ import { LazyValue } from '@bessemer/cornerstone/lazy'
3
+ import { Nil } from '@bessemer/cornerstone/types'
4
+
5
+ export function isUnreachable(message: LazyValue<string> = 'Preconditions.isUnreachable was reached'): never {
6
+ throw new Error(Lazy.evaluate(message))
7
+ }
8
+
9
+ export function isTrue(value: boolean, message: LazyValue<string> = 'Preconditions.isTrue failed validation'): asserts value is true {
10
+ if (!value) {
11
+ throw new Error(Lazy.evaluate(message))
12
+ }
13
+ }
14
+
15
+ export function isFalse(value: boolean, message: LazyValue<string> = 'Preconditions.isFalse failed validation'): asserts value is false {
16
+ return isTrue(!value, message)
17
+ }
18
+
19
+ export function isNil(value: any, message: LazyValue<string> = 'Preconditions.isNil failed validation'): asserts value is Nil {
20
+ return isTrue(Objects.isNil(value), message)
21
+ }
22
+
23
+ export function isPresent<T>(value: T, message: LazyValue<string> = 'Preconditions.isPresent failed validation'): asserts value is NonNullable<T> {
24
+ return isTrue(Objects.isPresent(value), message)
25
+ }
package/src/promise.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type PromiseContext<T> = { promise: Promise<T>; resolve: (value: T) => void; reject: (reason?: any) => void }
2
+
3
+ export const isPromise = <T>(element: T | Promise<T>): element is Promise<T> => {
4
+ return typeof (element as Promise<T>).then === 'function'
5
+ }
6
+
7
+ export const create = <T>(): PromiseContext<T> => {
8
+ let resolveVar
9
+ let rejectVar
10
+ const promise = new Promise<T>((resolve, reject) => {
11
+ resolveVar = resolve
12
+ rejectVar = reject
13
+ })
14
+
15
+ return { promise, resolve: resolveVar!, reject: rejectVar! }
16
+ }