@strictly/base 0.0.1

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 (114) hide show
  1. package/.eslintrc.cjs +26 -0
  2. package/.out/errors/not_implemented.d.ts +3 -0
  3. package/.out/errors/not_implemented.js +5 -0
  4. package/.out/errors/unexpected_implementation.d.ts +3 -0
  5. package/.out/errors/unexpected_implementation.js +5 -0
  6. package/.out/errors/unreachable.d.ts +3 -0
  7. package/.out/errors/unreachable.js +6 -0
  8. package/.out/index.d.ts +20 -0
  9. package/.out/index.js +20 -0
  10. package/.out/test/index.d.ts +1 -0
  11. package/.out/test/index.js +1 -0
  12. package/.out/test/vitest/expect.d.ts +4 -0
  13. package/.out/test/vitest/expect.js +13 -0
  14. package/.out/tsconfig.json +16 -0
  15. package/.out/tsconfig.tsbuildinfo +1 -0
  16. package/.out/tsup.config.d.ts +3 -0
  17. package/.out/tsup.config.js +12 -0
  18. package/.out/types/element_of_array.d.ts +1 -0
  19. package/.out/types/element_of_array.js +1 -0
  20. package/.out/types/extract_generics.d.ts +2 -0
  21. package/.out/types/extract_generics.js +1 -0
  22. package/.out/types/is_field_optional.d.ts +1 -0
  23. package/.out/types/is_field_optional.js +1 -0
  24. package/.out/types/is_field_readonly.d.ts +8 -0
  25. package/.out/types/is_field_readonly.js +2 -0
  26. package/.out/types/maybe.d.ts +3 -0
  27. package/.out/types/maybe.js +1 -0
  28. package/.out/types/printable_of.d.ts +1 -0
  29. package/.out/types/printable_of.js +1 -0
  30. package/.out/types/required_of_record.d.ts +3 -0
  31. package/.out/types/required_of_record.js +1 -0
  32. package/.out/types/specs/element_of_array.tests.d.ts +1 -0
  33. package/.out/types/specs/element_of_array.tests.js +6 -0
  34. package/.out/types/specs/is_field_readonly.tests.d.ts +1 -0
  35. package/.out/types/specs/is_field_readonly.tests.js +9 -0
  36. package/.out/types/specs/printable_of.tests.d.ts +1 -0
  37. package/.out/types/specs/printable_of.tests.js +6 -0
  38. package/.out/types/specs/required_of_record.tests.d.ts +1 -0
  39. package/.out/types/specs/required_of_record.tests.js +12 -0
  40. package/.out/types/specs/string_key_of.tests.d.ts +1 -0
  41. package/.out/types/specs/string_key_of.tests.js +10 -0
  42. package/.out/types/string_key_of.d.ts +1 -0
  43. package/.out/types/string_key_of.js +1 -0
  44. package/.out/util/array.d.ts +1 -0
  45. package/.out/util/array.js +8 -0
  46. package/.out/util/cache.d.ts +13 -0
  47. package/.out/util/cache.js +42 -0
  48. package/.out/util/delay.d.ts +12 -0
  49. package/.out/util/delay.js +32 -0
  50. package/.out/util/format.d.ts +2 -0
  51. package/.out/util/format.js +11 -0
  52. package/.out/util/json.d.ts +1 -0
  53. package/.out/util/json.js +11 -0
  54. package/.out/util/poll.d.ts +8 -0
  55. package/.out/util/poll.js +23 -0
  56. package/.out/util/preconditions.d.ts +11 -0
  57. package/.out/util/preconditions.js +43 -0
  58. package/.out/util/promises.d.ts +1 -0
  59. package/.out/util/promises.js +11 -0
  60. package/.out/util/record.d.ts +10 -0
  61. package/.out/util/record.js +64 -0
  62. package/.out/util/specs/cache.tests.d.ts +1 -0
  63. package/.out/util/specs/cache.tests.js +49 -0
  64. package/.out/util/specs/format.tests.d.ts +1 -0
  65. package/.out/util/specs/format.tests.js +67 -0
  66. package/.out/util/specs/poll.tests.d.ts +1 -0
  67. package/.out/util/specs/poll.tests.js +47 -0
  68. package/.out/vitest.workspace.d.ts +2 -0
  69. package/.out/vitest.workspace.js +7 -0
  70. package/.turbo/turbo-build.log +18 -0
  71. package/.turbo/turbo-check-types.log +3 -0
  72. package/.turbo/turbo-release$colon$exports.log +3 -0
  73. package/README.md +3 -0
  74. package/dist/index.cjs +387 -0
  75. package/dist/index.d.cts +109 -0
  76. package/dist/index.d.ts +109 -0
  77. package/dist/index.js +328 -0
  78. package/errors/not_implemented.ts +5 -0
  79. package/errors/unexpected_implementation.ts +5 -0
  80. package/errors/unreachable.ts +6 -0
  81. package/index.ts +20 -0
  82. package/package.exports.json +18 -0
  83. package/package.json +50 -0
  84. package/test/index.ts +1 -0
  85. package/test/vitest/expect.ts +16 -0
  86. package/tsconfig.build.json +11 -0
  87. package/tsconfig.json +16 -0
  88. package/tsup.config.ts +16 -0
  89. package/types/element_of_array.ts +1 -0
  90. package/types/extract_generics.ts +2 -0
  91. package/types/is_field_optional.ts +5 -0
  92. package/types/is_field_readonly.ts +8 -0
  93. package/types/maybe.ts +5 -0
  94. package/types/printable_of.ts +1 -0
  95. package/types/required_of_record.ts +4 -0
  96. package/types/specs/element_of_array.tests.ts +9 -0
  97. package/types/specs/is_field_readonly.tests.ts +15 -0
  98. package/types/specs/printable_of.tests.ts +8 -0
  99. package/types/specs/required_of_record.tests.ts +16 -0
  100. package/types/specs/string_key_of.tests.ts +16 -0
  101. package/types/string_key_of.ts +1 -0
  102. package/util/array.ts +12 -0
  103. package/util/cache.ts +52 -0
  104. package/util/delay.ts +36 -0
  105. package/util/format.ts +23 -0
  106. package/util/json.ts +10 -0
  107. package/util/poll.ts +40 -0
  108. package/util/preconditions.ts +83 -0
  109. package/util/promises.ts +11 -0
  110. package/util/record.ts +133 -0
  111. package/util/specs/cache.tests.ts +68 -0
  112. package/util/specs/format.tests.ts +70 -0
  113. package/util/specs/poll.tests.ts +75 -0
  114. package/vitest.workspace.ts +11 -0
package/util/delay.ts ADDED
@@ -0,0 +1,36 @@
1
+ export type Delay = () => Promise<void>
2
+
3
+ export function createDelay(millis: number): Delay {
4
+ return function () {
5
+ return delay(millis)
6
+ }
7
+ }
8
+
9
+ /**
10
+ * creates a delay that blocks everything for the specified number of cold milliseconds initially, then
11
+ * the warm millis thereafter
12
+ * @param coldMillis the number of milliseconds to block initial delays for
13
+ * @param warmMillis the number of milliseconds to block subsequent delays for
14
+ * @returns a Promise that awaits for the specified time
15
+ */
16
+ export function createWarmupDelay(coldMillis: number, warmMillis: number) {
17
+ let warmup: Promise<void> | undefined
18
+ return function () {
19
+ if (warmup != null) {
20
+ return warmup.then(function () {
21
+ return delay(warmMillis)
22
+ })
23
+ } else {
24
+ warmup = delay(coldMillis)
25
+ return warmup
26
+ }
27
+ }
28
+ }
29
+
30
+ export function delay(millis: number): Promise<void> {
31
+ return new Promise(function (resolve) {
32
+ setTimeout(resolve, millis)
33
+ })
34
+ }
35
+
36
+ export const secondDelay = createDelay(1000)
package/util/format.ts ADDED
@@ -0,0 +1,23 @@
1
+ export type FormatArg =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | object
6
+ | null
7
+ | undefined
8
+ | symbol
9
+
10
+ export function format(
11
+ message: string,
12
+ ...args: readonly FormatArg[]
13
+ ): string {
14
+ let index = 0
15
+ return message.replaceAll(/{(\d*)}/g, function (_substring: string, indexString: string) {
16
+ let argIndex = parseInt(indexString)
17
+ if (Number.isNaN(argIndex)) {
18
+ argIndex = index
19
+ index++
20
+ }
21
+ return JSON.stringify(args[argIndex])
22
+ })
23
+ }
package/util/json.ts ADDED
@@ -0,0 +1,10 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ export function errorHandlingJsonParse<T = any>(json: string, errorHandler?: (e: unknown) => void): T | null {
3
+ try {
4
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
5
+ return JSON.parse(json) as T
6
+ } catch (e) {
7
+ errorHandler?.(e)
8
+ return null
9
+ }
10
+ }
package/util/poll.ts ADDED
@@ -0,0 +1,40 @@
1
+ import {
2
+ type Maybe,
3
+ } from 'types/maybe'
4
+ import { delay } from './delay'
5
+
6
+ export function constantPollInterval(delay: number) {
7
+ return function () {
8
+ return delay
9
+ }
10
+ }
11
+
12
+ type PollOptions = {
13
+ pollInterval?: (retries: number) => number,
14
+ retries?: number,
15
+ }
16
+
17
+ export async function poll<T>(
18
+ f: () => Promise<Maybe<T>>,
19
+ {
20
+ pollInterval = constantPollInterval(200),
21
+ retries = 3,
22
+ }: PollOptions = {},
23
+ ): Promise<Maybe<T>> {
24
+ let retriesRemaining = retries
25
+ while (retriesRemaining > 0) {
26
+ await delay(pollInterval(retriesRemaining))
27
+ retriesRemaining--
28
+ const v = await f()
29
+ if (v != null) {
30
+ return v
31
+ }
32
+ }
33
+ return null
34
+ }
35
+
36
+ // TODO
37
+ // function createPoll<P, T>(
38
+ // p: (params: P) => Promise<Maybe<T>>,
39
+ // options: PollOptions,
40
+ // ): (params: P) => Promise<Maybe<T>>;
@@ -0,0 +1,83 @@
1
+ import {
2
+ format,
3
+ type FormatArg,
4
+ } from './format'
5
+
6
+ export class PreconditionFailedError extends Error {
7
+ constructor(message: string, ...args: readonly FormatArg[]) {
8
+ super(format(message, ...args))
9
+ this.name = 'PreconditionFailedError'
10
+ }
11
+ }
12
+
13
+ export function assertExistsAndReturn<T>(
14
+ t: T,
15
+ message: string,
16
+ ...args: readonly FormatArg[]
17
+ ): NonNullable<T> {
18
+ assertExists(t, message, ...args)
19
+ return t
20
+ }
21
+
22
+ export function assertExists<V>(v: V, message: string, ...args: readonly FormatArg[]): asserts v is NonNullable<V> {
23
+ if (v == null) {
24
+ throw new PreconditionFailedError(message, ...args)
25
+ }
26
+ }
27
+
28
+ export function assertEqual<T extends FormatArg>(
29
+ a: T,
30
+ b: T,
31
+ message: string = '{} != {}',
32
+ arg1: FormatArg = a,
33
+ arg2: FormatArg = b,
34
+ ...args: readonly FormatArg[]
35
+ ) {
36
+ if (a !== b) {
37
+ throw new PreconditionFailedError(
38
+ message,
39
+ arg1,
40
+ arg2,
41
+ ...args,
42
+ )
43
+ }
44
+ }
45
+
46
+ export function assertState(
47
+ condition: boolean,
48
+ message: string,
49
+ ...args: readonly FormatArg[]
50
+ ): asserts condition is true {
51
+ if (!condition) {
52
+ throw new PreconditionFailedError(message, ...args)
53
+ }
54
+ }
55
+
56
+ export function assertIs<V, T extends V>(
57
+ v: V,
58
+ condition: (v: V) => v is T,
59
+ message: string,
60
+ ...args: readonly FormatArg[]
61
+ ): asserts v is T {
62
+ if (!condition(v)) {
63
+ throw new PreconditionFailedError(message, ...args)
64
+ }
65
+ }
66
+
67
+ export function checkUnary<T>(
68
+ t: readonly T[],
69
+ message: string,
70
+ ...args: readonly FormatArg[]
71
+ ): T {
72
+ if (t.length !== 1) {
73
+ throw new PreconditionFailedError(message, ...args)
74
+ }
75
+ return t[0]
76
+ }
77
+
78
+ export function checkValidNumber(n: number, message: string, ...args: readonly FormatArg[]): number {
79
+ if (isNaN(n) || !isFinite(n)) {
80
+ throw new PreconditionFailedError(message, ...args)
81
+ }
82
+ return n
83
+ }
@@ -0,0 +1,11 @@
1
+ export function callAsPromise(f: (cb: (e?: unknown) => void) => void): Promise<void> {
2
+ return new Promise<void>(function (resolve, reject) {
3
+ f(function (e?: unknown) {
4
+ if (e == null) {
5
+ resolve()
6
+ }
7
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
8
+ reject(e)
9
+ })
10
+ })
11
+ }
package/util/record.ts ADDED
@@ -0,0 +1,133 @@
1
+ export function reverse<
2
+ Key extends string | number | symbol,
3
+ Value extends string | number | symbol,
4
+ >(obj: Record<Key, Value>): Record<Value, Key> {
5
+ return Object.keys(obj).reduce((acc, stringKey) => {
6
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
7
+ const key = stringKey as Key
8
+ const value = obj[key]
9
+ acc[value] = key
10
+ return acc
11
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
12
+ }, {} as Record<Value, Key>)
13
+ }
14
+
15
+ // TODO simplify the generics
16
+ export function rollup<
17
+ R extends Record<K, V>,
18
+ K extends string | number | symbol = keyof R,
19
+ V = R[K],
20
+ >(...records: Partial<R>[]): R {
21
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
22
+ return records.slice(1).reduce<Partial<R>>((acc, record) => {
23
+ Object.keys(record).forEach((key) => {
24
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
25
+ const k = key as K
26
+ acc[k] = acc[k] ?? record[k]
27
+ })
28
+ return acc
29
+ }, records[0]) as R
30
+ }
31
+
32
+ // TODO simplify the generics
33
+ export function union<
34
+ R1 extends Readonly<Record<K1, V1>>,
35
+ K1 extends string | number | symbol,
36
+ V1 extends R1[K1],
37
+ R2 extends Readonly<Record<K2, V2>>,
38
+ K2 extends string | number | symbol,
39
+ V2 extends R2[K2],
40
+ >(
41
+ r1: R1,
42
+ r2: R2,
43
+ ): R1 & R2 {
44
+ return {
45
+ ...r1,
46
+ ...r2,
47
+ }
48
+ }
49
+
50
+ // TODO simplify the generics
51
+ export function map<
52
+ K extends string | number | symbol,
53
+ V,
54
+ R = V,
55
+ >(
56
+ r: Readonly<Record<K, V>>,
57
+ f: (k: K, v: V) => R,
58
+ ): Record<K, R> {
59
+ // TODO can use reduce to implement map
60
+ return Object.entries<V>(r).reduce(
61
+ function (acc, [
62
+ k,
63
+ v,
64
+ ]) {
65
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
66
+ const typedKey = k as K
67
+ acc[typedKey] = f(typedKey, v)
68
+ return acc
69
+ },
70
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
71
+ {} as Record<K, R>,
72
+ )
73
+ }
74
+
75
+ // TODO simplify the generics
76
+ export function reduce<
77
+ K extends string | number | symbol,
78
+ V,
79
+ A,
80
+ >(
81
+ r: Readonly<Record<K, V>>,
82
+ f: (acc: A, k: K, v: V) => A,
83
+ a: A,
84
+ ): A {
85
+ return Object.entries<V>(r).reduce(
86
+ function (acc, [
87
+ k,
88
+ v,
89
+ ]) {
90
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
91
+ const typedKey = k as K
92
+ return f(acc, typedKey, v)
93
+ },
94
+ a,
95
+ )
96
+ }
97
+
98
+ export function forEach<
99
+ R extends Readonly<Record<K, V>>,
100
+ K extends string | number | symbol = R extends Readonly<Record<infer Kk, infer _Vv>> ? Kk : never,
101
+ V = R extends Readonly<Record<infer _Kk, infer Vv>> ? Vv : never,
102
+ >(r: R, f: (k: K, v: R[K]) => void) {
103
+ return Object.entries<V>(r).forEach(
104
+ function ([
105
+ k,
106
+ v,
107
+ ]) {
108
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
109
+ return f(k as K, v as R[K])
110
+ },
111
+ )
112
+ }
113
+
114
+ export function toArray<
115
+ K extends string | number | symbol,
116
+ V,
117
+ >(r: Readonly<Record<K, V>>): readonly (readonly [K, V])[] {
118
+ return reduce<K, V, [K, V][]>(
119
+ r,
120
+ function (acc, k, v) {
121
+ acc.push([
122
+ k,
123
+ v,
124
+ ])
125
+ return acc
126
+ },
127
+ [],
128
+ )
129
+ }
130
+
131
+ export type Mutable<T> = {
132
+ -readonly [K in keyof T]: T[K]
133
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ Cache,
3
+ type CacheValueFactory,
4
+ } from 'util/cache'
5
+ import {
6
+ type Mock,
7
+ vi,
8
+ } from 'vitest'
9
+
10
+ describe('cache', function () {
11
+ type Args = [string, number, boolean]
12
+ let cache: Cache<Args, boolean>
13
+ let valueFactory: Mock<CacheValueFactory<Args, boolean>>
14
+
15
+ beforeEach(function () {
16
+ valueFactory = vi.fn()
17
+ valueFactory.mockReturnValue(true)
18
+ cache = new Cache(
19
+ valueFactory,
20
+ )
21
+ })
22
+
23
+ describe('creates value that does not exist', function () {
24
+ let value: boolean
25
+ const params: Args = [
26
+ 'a',
27
+ 1,
28
+ false,
29
+ ]
30
+ beforeEach(function () {
31
+ value = cache.retrieveOrCreate(...params)
32
+ })
33
+
34
+ it('calls the value factory with the transformed key', function () {
35
+ expect(valueFactory).toHaveBeenCalledOnce()
36
+ expect(valueFactory).toHaveBeenCalledWith(...params)
37
+ })
38
+
39
+ it('returns the expected value', function () {
40
+ expect(value).toBeTruthy()
41
+ })
42
+
43
+ describe('retrieveByKey', function () {
44
+ it('retrieves value by key', function () {
45
+ expect(cache.retrieve(...params)).toEqual([true])
46
+ })
47
+
48
+ it('returns nothing when a non-existent key is supplied', function () {
49
+ expect(cache.retrieve('a', 1, true)).toBeNull()
50
+ })
51
+ })
52
+
53
+ describe('looking up previously created value', function () {
54
+ let cachedValue: boolean
55
+ beforeEach(function () {
56
+ cachedValue = cache.retrieveOrCreate(...params)
57
+ })
58
+
59
+ it('does not create the value again', function () {
60
+ expect(valueFactory).toHaveBeenCalledOnce()
61
+ })
62
+
63
+ it('returns the expected value', function () {
64
+ expect(cachedValue).toBeTruthy()
65
+ })
66
+ })
67
+ })
68
+ })
@@ -0,0 +1,70 @@
1
+ import { format } from 'util/format'
2
+
3
+ describe('format', function () {
4
+ it.each(
5
+ [
6
+ [
7
+ 'no args',
8
+ 'message',
9
+ 'message',
10
+ ],
11
+ [
12
+ 'one anonymous arg',
13
+ 'arg {}',
14
+ 'arg 1',
15
+ 1,
16
+ ],
17
+ [
18
+ 'one indexed arg',
19
+ 'arg {0}',
20
+ 'arg "0"',
21
+ '0',
22
+ ],
23
+ [
24
+ 'two args',
25
+ 'args {}, {}',
26
+ 'args 1, 2',
27
+ 1,
28
+ 2,
29
+ ],
30
+ [
31
+ 'two indexed args',
32
+ 'args {0}, {1}',
33
+ 'args 1, 2',
34
+ 1,
35
+ 2,
36
+ ],
37
+ [
38
+ 'two indexed args, reversed',
39
+ 'args {1}, {0}',
40
+ 'args 2, 1',
41
+ 1,
42
+ 2,
43
+ ],
44
+ [
45
+ 'null',
46
+ 'null? {}',
47
+ 'null? null',
48
+ null,
49
+ ],
50
+ [
51
+ 'mixed argument types',
52
+ '{} {} {} {}',
53
+ '1 "2" true -5',
54
+ 1,
55
+ '2',
56
+ true,
57
+ -5,
58
+ ],
59
+ [
60
+ 'object',
61
+ '{}',
62
+ '{"b":true}',
63
+ { b: true },
64
+ ],
65
+ ] as const,
66
+ )('formats %s', function (_name, message, expectedResult, ...args) {
67
+ const result = format(message, ...args)
68
+ expect(result).toEqual(expectedResult)
69
+ })
70
+ })
@@ -0,0 +1,75 @@
1
+ import {
2
+ type Maybe,
3
+ } from 'types/maybe'
4
+ import {
5
+ constantPollInterval,
6
+ poll,
7
+ } from 'util/poll'
8
+ import {
9
+ type Mock,
10
+ vi,
11
+ } from 'vitest'
12
+
13
+ describe('poll', function () {
14
+ const pollInterval = constantPollInterval(1)
15
+ let callee: Mock<() => Promise<Maybe<number>>>
16
+
17
+ beforeEach(function () {
18
+ callee = vi.fn()
19
+ })
20
+
21
+ it('returns the success value', async function () {
22
+ const value = 1
23
+ callee.mockResolvedValueOnce([value])
24
+ const result = await poll(
25
+ callee,
26
+ {
27
+ pollInterval,
28
+ },
29
+ )
30
+ expect(result).toEqual([value])
31
+ expect(callee).toHaveBeenCalledTimes(1)
32
+ })
33
+
34
+ it('returns the success value after a retry', async function () {
35
+ const value = 1
36
+ callee.mockResolvedValueOnce(null)
37
+ callee.mockResolvedValueOnce([value])
38
+ const result = await poll(
39
+ callee,
40
+ {
41
+ pollInterval,
42
+ retries: 2,
43
+ },
44
+ )
45
+ expect(result).toEqual([value])
46
+ expect(callee).toHaveBeenCalledTimes(2)
47
+ })
48
+
49
+ it('returns null when polling result not available after retries', async function () {
50
+ callee.mockResolvedValue(null)
51
+ const retries = 4
52
+ const result = await poll(
53
+ callee,
54
+ {
55
+ pollInterval,
56
+ retries,
57
+ },
58
+ )
59
+ expect(result).toBe(null)
60
+ expect(callee).toHaveBeenCalledTimes(retries)
61
+ })
62
+
63
+ it('reports errors immediately', async function () {
64
+ const error = new Error()
65
+ callee.mockRejectedValue(error)
66
+
67
+ await expect(poll(
68
+ callee,
69
+ {
70
+ pollInterval,
71
+ },
72
+ )).rejects.toBe(error)
73
+ expect(callee).toHaveBeenCalledTimes(1)
74
+ })
75
+ })
@@ -0,0 +1,11 @@
1
+ import { createVitestUserConfig } from '@strictly/support-vite'
2
+ import {
3
+ defineWorkspace,
4
+ } from 'vitest/config'
5
+
6
+ import tsconfig from './tsconfig.json'
7
+
8
+ export default defineWorkspace([
9
+ '.',
10
+ createVitestUserConfig(tsconfig),
11
+ ] as const)