@pippenly/ts-utils 1.0.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/.github/workflows/npm-publish-github-packages.yml +36 -0
- package/README.md +39 -0
- package/jest.config.ts +17 -0
- package/package.json +54 -0
- package/src/dom/index.ts +189 -0
- package/src/index.ts +2 -0
- package/src/internal/index.ts +0 -0
- package/src/result/async/index.ts +77 -0
- package/src/result/index.ts +2 -0
- package/src/result/sync/index.ts +140 -0
- package/src/structures/index.ts +6 -0
- package/src/structures/queue/bounded/index.ts +121 -0
- package/src/structures/queue/index.ts +2 -0
- package/src/structures/queue/queue.ts +52 -0
- package/src/structures/queue/unbounded/index.ts +5 -0
- package/src/utils/index.ts +23 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +56 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
+
|
|
4
|
+
name: Node.js Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 24
|
|
18
|
+
- run: npm ci
|
|
19
|
+
- run: npm test
|
|
20
|
+
|
|
21
|
+
publish-gpr:
|
|
22
|
+
needs: build
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
packages: write
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
node-version: 24
|
|
32
|
+
registry-url: https://npm.pkg.github.com/
|
|
33
|
+
- run: npm ci
|
|
34
|
+
- run: npm publish
|
|
35
|
+
env:
|
|
36
|
+
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# TS-Utils
|
|
2
|
+
|
|
3
|
+
Library where I implement ideas I have or consolidate utilities/helpers I use often
|
|
4
|
+
|
|
5
|
+
# DOM
|
|
6
|
+
|
|
7
|
+
## Resin
|
|
8
|
+
A reactive state that can bind to an HTML element to automatically apply updates based on value changes. Has some utilities for controlling visibility, mapping inner values, and passes the default element event listener through so you can use those normally.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const number = resin(5); // Resin<T>;
|
|
12
|
+
const numberElment = document.getElementById("number")!; // HTMLElement
|
|
13
|
+
const otherNumberElement = document.getElementById("other-number")!; // HTMLElement
|
|
14
|
+
const binding = bind(number, numberElment); // BoundResin<T>
|
|
15
|
+
const complexBinding = bind(number, otherNumberElement, {
|
|
16
|
+
type: "innerText",
|
|
17
|
+
map: (n) => n * 2,
|
|
18
|
+
if: (n) => n > 5,
|
|
19
|
+
class: {
|
|
20
|
+
"red": (_, n) => n > 10,
|
|
21
|
+
"blue": (_, n) => n <= 10
|
|
22
|
+
},
|
|
23
|
+
attr: {
|
|
24
|
+
"data-value": (_, n) => String(n)
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
binding.value = 15; // Updates number.value, which triggers the effect to update numberElement's innerText
|
|
29
|
+
complexBinding.value = 15;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
# Result
|
|
33
|
+
Small utility library so I can work with error as values and explict Error type declaration
|
|
34
|
+
|
|
35
|
+
# Structures
|
|
36
|
+
Queue only so far, it's what I found myself using the most often
|
|
37
|
+
|
|
38
|
+
# Utils
|
|
39
|
+
Quality of life things
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Config } from 'jest';
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
preset: 'ts-jest/presets/default-esm',
|
|
5
|
+
testEnvironment: 'node',
|
|
6
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
7
|
+
moduleNameMapper: {
|
|
8
|
+
// Rewrite .js extension imports to allow jest to resolve .ts source files
|
|
9
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
10
|
+
},
|
|
11
|
+
transform: {
|
|
12
|
+
'^.+\\.tsx?$': ['ts-jest', { useESM: true }],
|
|
13
|
+
},
|
|
14
|
+
testMatch: ['**/tests/**/*.test.ts'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default config;
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pippenly/ts-utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./result": {
|
|
14
|
+
"types": "./dist/result/index.d.ts",
|
|
15
|
+
"import": "./dist/result/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./result/sync": {
|
|
18
|
+
"types": "./dist/result/sync/index.d.ts",
|
|
19
|
+
"import": "./dist/result/sync/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./result/async": {
|
|
22
|
+
"types": "./dist/result/async/index.d.ts",
|
|
23
|
+
"import": "./dist/result/async/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./utils": {
|
|
26
|
+
"types": "./dist/utils/index.d.ts",
|
|
27
|
+
"import": "./dist/utils/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./dom": {
|
|
30
|
+
"types": "./dist/dom/index.d.ts",
|
|
31
|
+
"import": "./dist/dom/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json && tsc-alias",
|
|
36
|
+
"prebuild": "rm -rf dist",
|
|
37
|
+
"clean": "rm -rf dist",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests",
|
|
40
|
+
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch --passWithNoTests"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [],
|
|
43
|
+
"author": "",
|
|
44
|
+
"license": "ISC",
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/jest": "^30.0.0",
|
|
47
|
+
"@types/node": "^25.5.2",
|
|
48
|
+
"jest": "^30.3.0",
|
|
49
|
+
"ts-jest": "^29.4.9",
|
|
50
|
+
"tsc-alias": "^1.8.16",
|
|
51
|
+
"typescript": "^6.0.2",
|
|
52
|
+
"uuid": "^13.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/dom/index.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Err, Ok, type Result } from "@/result/index.js";
|
|
2
|
+
|
|
3
|
+
type State<T> = { value: T };
|
|
4
|
+
|
|
5
|
+
type Resin<T> = State<T> & { bound: false };
|
|
6
|
+
|
|
7
|
+
type BoundState<E extends HTMLElement> = {
|
|
8
|
+
bound: true;
|
|
9
|
+
element: E;
|
|
10
|
+
dispose: () => Result<void, string>;
|
|
11
|
+
} & Pick<HTMLElement, "addEventListener" | "removeEventListener">;
|
|
12
|
+
|
|
13
|
+
type BoundResin<T> = State<T> & BoundState<HTMLElement>;
|
|
14
|
+
|
|
15
|
+
type ElementBindOptions<T, E extends HTMLElement, R = T> = {
|
|
16
|
+
map?: (value: T) => R;
|
|
17
|
+
if?: (value: R) => boolean;
|
|
18
|
+
filter?: (value: R) => boolean;
|
|
19
|
+
class?: {
|
|
20
|
+
[className: string]: (element: E, value: R) => boolean;
|
|
21
|
+
},
|
|
22
|
+
attr?: {
|
|
23
|
+
[attrName: string]: (element: E, value: R) => string | null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type TextElementTypes =
|
|
28
|
+
| HTMLParagraphElement
|
|
29
|
+
| HTMLSpanElement
|
|
30
|
+
| HTMLDivElement
|
|
31
|
+
| HTMLHeadingElement
|
|
32
|
+
| HTMLAnchorElement
|
|
33
|
+
| HTMLButtonElement
|
|
34
|
+
| HTMLLabelElement
|
|
35
|
+
| HTMLOptionElement
|
|
36
|
+
| HTMLTableCellElement;
|
|
37
|
+
|
|
38
|
+
type TextBindOptions<T, E extends TextElementTypes> = ElementBindOptions<T, E> & {
|
|
39
|
+
type: 'innerText' | 'textContent' | 'innerHTML';
|
|
40
|
+
truncate?: number;
|
|
41
|
+
mask?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ValueBindOptions<T, E extends HTMLElement> = ElementBindOptions<T, E> & {
|
|
45
|
+
type: 'value';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type BindOptions<T, E extends HTMLElement> =
|
|
49
|
+
| TextBindOptions<T, E>
|
|
50
|
+
| ValueBindOptions<T, E>;
|
|
51
|
+
|
|
52
|
+
export function resin<T>(initialValue: T): Resin<T> {
|
|
53
|
+
return { value: initialValue, bound: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function bind<T, E extends TextElementTypes>(resin: Resin<T>, element: E, options?: TextBindOptions<T, E>): BoundResin<T>;
|
|
57
|
+
export function bind<T, E extends HTMLElement>(resin: Resin<T>, element: E, options?: ValueBindOptions<T, E>): BoundResin<T>;
|
|
58
|
+
export function bind<T, E extends HTMLElement>(resin: Resin<T>, element: E, options?: BindOptions<T, E>): BoundResin<T> {
|
|
59
|
+
const subscribers = new Set<Effect>();
|
|
60
|
+
let disposed = false;
|
|
61
|
+
let _value = resin.value;
|
|
62
|
+
|
|
63
|
+
const notify = () => {
|
|
64
|
+
if (disposed) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
subscribers.forEach(fn => fn());
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getValue = (): T => {
|
|
72
|
+
const currentEffect = stack[stack.length - 1];
|
|
73
|
+
if (currentEffect) {
|
|
74
|
+
subscribers.add(currentEffect);
|
|
75
|
+
}
|
|
76
|
+
return _value;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const domEffect = () => {
|
|
80
|
+
const value = getValue();
|
|
81
|
+
const mappedValue = options?.map ? options.map(value) : value;
|
|
82
|
+
|
|
83
|
+
if (options?.filter && !options.filter(mappedValue)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (options?.if) {
|
|
88
|
+
if (!options.if(mappedValue as ReturnType<NonNullable<typeof options.map>>)) {
|
|
89
|
+
element.style.display = "none";
|
|
90
|
+
return;
|
|
91
|
+
} else {
|
|
92
|
+
element.style.display = element.style.display || "";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (options?.class) {
|
|
97
|
+
for (const [className, fn] of Object.entries(options.class)) {
|
|
98
|
+
if (fn(element, mappedValue)) {
|
|
99
|
+
element.classList.add(className);
|
|
100
|
+
} else {
|
|
101
|
+
element.classList.remove(className);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (options?.attr) {
|
|
107
|
+
for (const [attrName, fn] of Object.entries(options.attr)) {
|
|
108
|
+
const attrValue = fn(element, mappedValue);
|
|
109
|
+
if (attrValue === null) {
|
|
110
|
+
element.removeAttribute(attrName);
|
|
111
|
+
} else {
|
|
112
|
+
element.setAttribute(attrName, attrValue);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let valueStr = String(mappedValue);
|
|
118
|
+
if (options?.type === 'innerText' || options?.type === 'textContent') {
|
|
119
|
+
if (options?.truncate !== undefined) {
|
|
120
|
+
valueStr = valueStr.slice(0, options.truncate);
|
|
121
|
+
}
|
|
122
|
+
if (options?.mask !== undefined) {
|
|
123
|
+
valueStr = valueStr.replace(/./g, options.mask);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (options?.type === "innerText") {
|
|
128
|
+
element.innerText = valueStr;
|
|
129
|
+
} else if (options?.type === "textContent") {
|
|
130
|
+
element.textContent = valueStr;
|
|
131
|
+
} else if (options?.type === "innerHTML") {
|
|
132
|
+
element.innerHTML = valueStr;
|
|
133
|
+
} else if (options?.type === "value") {
|
|
134
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
135
|
+
element.value = valueStr;
|
|
136
|
+
} else {
|
|
137
|
+
throw new Error("Value binding is only supported on input, textarea, and select elements");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
watchEffect(domEffect);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...resin,
|
|
146
|
+
bound: true,
|
|
147
|
+
element,
|
|
148
|
+
addEventListener: element.addEventListener.bind(element),
|
|
149
|
+
removeEventListener: element.removeEventListener.bind(element),
|
|
150
|
+
get value(): T {
|
|
151
|
+
if (disposed) {
|
|
152
|
+
throw new Error("Cannot get value of a disposed resin");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return getValue();
|
|
156
|
+
},
|
|
157
|
+
set value(newValue: T) {
|
|
158
|
+
if (disposed) {
|
|
159
|
+
throw new Error("Cannot set value of a disposed resin");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (_value === newValue) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_value = newValue;
|
|
167
|
+
notify();
|
|
168
|
+
},
|
|
169
|
+
dispose: () => {
|
|
170
|
+
if (disposed) {
|
|
171
|
+
return Err("Resin is already disposed");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
subscribers.delete(domEffect);
|
|
175
|
+
disposed = true;
|
|
176
|
+
return Ok(undefined);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type Effect = () => void;
|
|
182
|
+
|
|
183
|
+
const stack: Effect[] = [];
|
|
184
|
+
|
|
185
|
+
export function watchEffect(fn: Effect): void {
|
|
186
|
+
stack.push(fn);
|
|
187
|
+
fn();
|
|
188
|
+
stack.pop();
|
|
189
|
+
}
|
package/src/index.ts
ADDED
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Ok, Err, isOk, isErr } from '../sync/index.js';
|
|
2
|
+
import type { Result } from '../sync/index.js';
|
|
3
|
+
|
|
4
|
+
export type AsyncResult<T, E> = Promise<Result<T, E>>;
|
|
5
|
+
|
|
6
|
+
export async function fromPromise<T, E>(
|
|
7
|
+
promise: Promise<T>,
|
|
8
|
+
onErr: (e: unknown) => E,
|
|
9
|
+
): AsyncResult<T, E> {
|
|
10
|
+
try {
|
|
11
|
+
return Ok(await promise);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
return Err(onErr(e));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function asyncAndThen<T, E, U>(
|
|
18
|
+
result: AsyncResult<T, E>,
|
|
19
|
+
fn: (value: T) => AsyncResult<U, E>,
|
|
20
|
+
): AsyncResult<U, E> {
|
|
21
|
+
const r = await result;
|
|
22
|
+
return isOk(r) ? fn(r.value) : r;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function asyncAndThenErr<T, E, F>(
|
|
26
|
+
result: AsyncResult<T, E>,
|
|
27
|
+
fn: (error: E) => AsyncResult<T, F>,
|
|
28
|
+
): AsyncResult<T, F> {
|
|
29
|
+
const r = await result;
|
|
30
|
+
return isErr(r) ? fn(r.error) : r;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function asyncMap<T, E, U>(
|
|
34
|
+
result: AsyncResult<T, E>,
|
|
35
|
+
fn: (value: T) => Promise<U>,
|
|
36
|
+
): AsyncResult<U, E> {
|
|
37
|
+
const r = await result;
|
|
38
|
+
return isOk(r) ? Ok(await fn(r.value)) : r;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function asyncMapErr<T, E, F>(
|
|
42
|
+
result: AsyncResult<T, E>,
|
|
43
|
+
fn: (error: E) => Promise<F>,
|
|
44
|
+
): AsyncResult<T, F> {
|
|
45
|
+
const r = await result;
|
|
46
|
+
return isErr(r) ? Err(await fn(r.error)) : r;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function asyncTap<T, E>(
|
|
50
|
+
result: AsyncResult<T, E>,
|
|
51
|
+
fn: (value: T) => Promise<void>,
|
|
52
|
+
): AsyncResult<T, E> {
|
|
53
|
+
const r = await result;
|
|
54
|
+
if (isOk(r)) await fn(r.value);
|
|
55
|
+
return r;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function asyncTapErr<T, E>(
|
|
59
|
+
result: AsyncResult<T, E>,
|
|
60
|
+
fn: (error: E) => Promise<void>,
|
|
61
|
+
): AsyncResult<T, E> {
|
|
62
|
+
const r = await result;
|
|
63
|
+
if (isErr(r)) await fn(r.error);
|
|
64
|
+
return r;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function asyncCombine<T, E>(results: AsyncResult<T, E>[]): AsyncResult<T[], E> {
|
|
68
|
+
const settled = await Promise.all(results);
|
|
69
|
+
const values: T[] = [];
|
|
70
|
+
for (const r of settled) {
|
|
71
|
+
if (isErr(r)) {
|
|
72
|
+
return r;
|
|
73
|
+
}
|
|
74
|
+
values.push(r.value);
|
|
75
|
+
}
|
|
76
|
+
return Ok(values);
|
|
77
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export type Ok<T> = {
|
|
2
|
+
ok: true;
|
|
3
|
+
value: T;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type Err<E> = {
|
|
7
|
+
ok: false;
|
|
8
|
+
error: E;
|
|
9
|
+
code?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type Result<T, E = never> = Ok<T> | Err<E>;
|
|
13
|
+
|
|
14
|
+
export function Ok<T>(value: T): Ok<T> {
|
|
15
|
+
return { ok: true, value };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Err<E>(error: E, code?: number): Err<E> {
|
|
19
|
+
return { ok: false, error, code };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function tryCatch<T, E>(fn: () => T, onErr: (e: unknown) => E): Result<T, E> {
|
|
23
|
+
try {
|
|
24
|
+
return Ok(fn());
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return Err(onErr(e));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function fromNullable<T, E>(value: T | null | undefined, error: E): Result<NonNullable<T>, E> {
|
|
31
|
+
return value != null ? Ok(value as NonNullable<T>) : Err(error);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
|
|
35
|
+
return result.ok === true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isErr<T, E>(result: Result<T, E>): result is Err<E> {
|
|
39
|
+
return result.ok === false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function unwrap<T, E>(result: Result<T, E>): T {
|
|
43
|
+
if (isOk(result)) return result.value;
|
|
44
|
+
throw new Error(`Tried to unwrap an Err: ${result.error}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function unwrapErr<T, E>(result: Result<T, E>): E {
|
|
48
|
+
if (isErr(result)) return result.error;
|
|
49
|
+
throw new Error(`Tried to unwrapErr an Ok: ${result.value}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function toNullable<T, E>(result: Result<T, E>): T | null {
|
|
53
|
+
return isOk(result) ? result.value : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function toUndefined<T, E>(result: Result<T, E>): T | undefined {
|
|
57
|
+
return isOk(result) ? result.value : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function orElse<T, E>(result: Result<T, E>, defaultValue: T): T {
|
|
61
|
+
return isOk(result) ? result.value : defaultValue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function orElseGet<T, E>(result: Result<T, E>, defaultValueFn: () => T): T {
|
|
65
|
+
return isOk(result) ? result.value : defaultValueFn();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function orElseErr<T, E>(result: Result<T, E>, defaultError: E): E {
|
|
69
|
+
return isErr(result) ? result.error : defaultError;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function orElseErrGet<T, E>(result: Result<T, E>, defaultErrorFn: () => E): E {
|
|
73
|
+
return isErr(result) ? result.error : defaultErrorFn();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function andThen<T, E, U>(result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E> {
|
|
77
|
+
return isOk(result) ? fn(result.value) : result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function andThenErr<T, E, F>(result: Result<T, E>, fn: (error: E) => Result<T, F>): Result<T, F> {
|
|
81
|
+
return isErr(result) ? fn(result.error) : result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function map<T, E, U>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {
|
|
85
|
+
return isOk(result) ? Ok(fn(result.value)) : result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function mapErr<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> {
|
|
89
|
+
return isErr(result) ? Err(fn(result.error)) : result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function mapOr<T, E, U>(result: Result<T, E>, defaultValue: U, fn: (value: T) => U): U {
|
|
93
|
+
return isOk(result) ? fn(result.value) : defaultValue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function mapOrElse<T, E, U>(result: Result<T, E>, defaultValueFn: () => U, fn: (value: T) => U): U {
|
|
97
|
+
return isOk(result) ? fn(result.value) : defaultValueFn();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function filter<T, E>(result: Result<T, E>, predicate: (value: T) => boolean, onFail: E): Result<T, E> {
|
|
101
|
+
if (isOk(result)) {
|
|
102
|
+
return predicate(result.value) ? result : Err(onFail);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function tap<T, E>(result: Result<T, E>, fn: (value: T) => void): Result<T, E> {
|
|
108
|
+
if (isOk(result)) fn(result.value);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function tapErr<T, E>(result: Result<T, E>, fn: (error: E) => void): Result<T, E> {
|
|
113
|
+
if (isErr(result)) fn(result.error);
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function combine<T, E>(results: Result<T, E>[]): Result<T[], E> {
|
|
118
|
+
const values: T[] = [];
|
|
119
|
+
for (const result of results) {
|
|
120
|
+
if (isErr(result)) return result;
|
|
121
|
+
values.push(result.value);
|
|
122
|
+
}
|
|
123
|
+
return Ok(values);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function fold<T, E, U>(result: Result<T, E>, onOk: (value: T) => U, onErr: (error: E) => U): U {
|
|
127
|
+
return isOk(result) ? onOk(result.value) : onErr(result.error);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function swap<T, E>(result: Result<T, E>): Result<E, T> {
|
|
131
|
+
return isOk(result) ? Err(result.value) : Ok(result.error);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function flatten<T, E>(result: Result<Result<T, E>, E>): Result<T, E> {
|
|
135
|
+
return isOk(result) ? result.value : result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function flattenErr<T, E>(result: Result<T, Result<T, E>>): Result<T, E> {
|
|
139
|
+
return isErr(result) ? result.error : result;
|
|
140
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Err, Ok, type Result } from "@/result/index.js";
|
|
2
|
+
import type { Queue } from "../queue.js";
|
|
3
|
+
|
|
4
|
+
function makeRingBuffer<T>(capacity: number) {
|
|
5
|
+
const buffer = new Array<T | undefined>(capacity);
|
|
6
|
+
let head = 0;
|
|
7
|
+
let tail = 0;
|
|
8
|
+
let length = 0;
|
|
9
|
+
|
|
10
|
+
function rawPush(item: T): void {
|
|
11
|
+
buffer[tail] = item;
|
|
12
|
+
tail = (tail + 1) % capacity;
|
|
13
|
+
length++;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rawPop(): T {
|
|
17
|
+
const value = buffer[head] as T;
|
|
18
|
+
buffer[head] = undefined;
|
|
19
|
+
head = (head + 1) % capacity;
|
|
20
|
+
length--;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
getLength: () => length,
|
|
26
|
+
rawPush,
|
|
27
|
+
rawPop,
|
|
28
|
+
pop(): Result<T, string> {
|
|
29
|
+
if (length === 0) return Err("Queue is empty");
|
|
30
|
+
return Ok(rawPop());
|
|
31
|
+
},
|
|
32
|
+
peek(): Result<T, string> {
|
|
33
|
+
return length > 0 ? Ok(buffer[head] as T) : Err("Queue is empty");
|
|
34
|
+
},
|
|
35
|
+
size(): number {
|
|
36
|
+
return length;
|
|
37
|
+
},
|
|
38
|
+
isEmpty(): boolean {
|
|
39
|
+
return length === 0;
|
|
40
|
+
},
|
|
41
|
+
iterator(): Iterator<T> {
|
|
42
|
+
let i = 0;
|
|
43
|
+
let pos = head;
|
|
44
|
+
const snap = length;
|
|
45
|
+
return {
|
|
46
|
+
next(): IteratorResult<T> {
|
|
47
|
+
if (i < snap) {
|
|
48
|
+
const value = buffer[pos] as T;
|
|
49
|
+
pos = (pos + 1) % capacity;
|
|
50
|
+
i++;
|
|
51
|
+
return { value, done: false };
|
|
52
|
+
}
|
|
53
|
+
return { value: undefined as unknown as T, done: true };
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function boundedQueue<T>(capacity: number): Result<Queue<T>, string> {
|
|
61
|
+
if (capacity <= 0) {
|
|
62
|
+
return Err("Capacity must be greater than 0");
|
|
63
|
+
}
|
|
64
|
+
const rb = makeRingBuffer<T>(capacity);
|
|
65
|
+
return Ok({
|
|
66
|
+
push(item: T): Result<void, string> {
|
|
67
|
+
if (rb.getLength() >= capacity) {
|
|
68
|
+
return Err("Queue is full");
|
|
69
|
+
}
|
|
70
|
+
rb.rawPush(item);
|
|
71
|
+
return Ok(undefined);
|
|
72
|
+
},
|
|
73
|
+
pop: () => rb.pop(),
|
|
74
|
+
peek: () => rb.peek(),
|
|
75
|
+
size: () => rb.size(),
|
|
76
|
+
isEmpty: () => rb.isEmpty(),
|
|
77
|
+
iterator: () => rb.iterator()
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function slidingQueue<T>(capacity: number): Result<Queue<T>, string> {
|
|
82
|
+
if (capacity <= 0) {
|
|
83
|
+
return Err("Capacity must be greater than 0");
|
|
84
|
+
}
|
|
85
|
+
const rb = makeRingBuffer<T>(capacity);
|
|
86
|
+
return Ok({
|
|
87
|
+
push(item: T): Result<void, string> {
|
|
88
|
+
if (rb.getLength() >= capacity) {
|
|
89
|
+
rb.rawPop();
|
|
90
|
+
}
|
|
91
|
+
rb.rawPush(item);
|
|
92
|
+
return Ok(undefined);
|
|
93
|
+
},
|
|
94
|
+
pop: () => rb.pop(),
|
|
95
|
+
peek: () => rb.peek(),
|
|
96
|
+
size: () => rb.size(),
|
|
97
|
+
isEmpty: () => rb.isEmpty(),
|
|
98
|
+
iterator: () => rb.iterator()
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function droppingQueue<T>(capacity: number): Result<Queue<T>, string> {
|
|
103
|
+
if (capacity <= 0) {
|
|
104
|
+
return Err("Capacity must be greater than 0");
|
|
105
|
+
}
|
|
106
|
+
const rb = makeRingBuffer<T>(capacity);
|
|
107
|
+
return Ok({
|
|
108
|
+
push(item: T): Result<void, string> {
|
|
109
|
+
if (rb.getLength() >= capacity) {
|
|
110
|
+
return Err("Queue is full");
|
|
111
|
+
}
|
|
112
|
+
rb.rawPush(item);
|
|
113
|
+
return Ok(undefined);
|
|
114
|
+
},
|
|
115
|
+
pop: () => rb.pop(),
|
|
116
|
+
peek: () => rb.peek(),
|
|
117
|
+
size: () => rb.size(),
|
|
118
|
+
isEmpty: () => rb.isEmpty(),
|
|
119
|
+
iterator: () => rb.iterator()
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Err, Ok, type Result } from "@/result/index.js";
|
|
2
|
+
|
|
3
|
+
export interface Queue<T> {
|
|
4
|
+
push(item: T): Result<void, string>;
|
|
5
|
+
pop(): Result<T, string>;
|
|
6
|
+
peek(): Result<T, string>;
|
|
7
|
+
size(): number;
|
|
8
|
+
isEmpty(): boolean;
|
|
9
|
+
iterator(): Iterator<T>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function queue<T>(): Queue<T> {
|
|
13
|
+
const map = new Map<number, T>();
|
|
14
|
+
let head = 0;
|
|
15
|
+
let tail = 0;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
push(item: T): Result<void, string> {
|
|
19
|
+
map.set(tail++, item);
|
|
20
|
+
return Ok(undefined);
|
|
21
|
+
},
|
|
22
|
+
pop(): Result<T, string> {
|
|
23
|
+
if (map.size === 0) {
|
|
24
|
+
return Err("Queue is empty");
|
|
25
|
+
}
|
|
26
|
+
const value = map.get(head)!;
|
|
27
|
+
map.delete(head++);
|
|
28
|
+
return Ok(value);
|
|
29
|
+
},
|
|
30
|
+
peek(): Result<T, string> {
|
|
31
|
+
return map.size > 0 ? Ok(map.get(head)!) : Err("Queue is empty");
|
|
32
|
+
},
|
|
33
|
+
size(): number {
|
|
34
|
+
return map.size;
|
|
35
|
+
},
|
|
36
|
+
isEmpty(): boolean {
|
|
37
|
+
return map.size === 0;
|
|
38
|
+
},
|
|
39
|
+
iterator(): Iterator<T> {
|
|
40
|
+
let current = head;
|
|
41
|
+
const end = tail;
|
|
42
|
+
return {
|
|
43
|
+
next(): IteratorResult<T> {
|
|
44
|
+
if (current < end && map.has(current)) {
|
|
45
|
+
return { value: map.get(current++)!, done: false };
|
|
46
|
+
}
|
|
47
|
+
return { value: undefined as unknown as T, done: true };
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
|
|
3
|
+
export const generateUUID = (): string => {
|
|
4
|
+
return uuidv4();
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const ENVIRONMENT = (() => {
|
|
8
|
+
if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
|
|
9
|
+
return "node";
|
|
10
|
+
} else {
|
|
11
|
+
return "browser";
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
|
|
15
|
+
export const eventToPromise = (eventTarget: EventTarget, eventName: string): Promise<Event> => {
|
|
16
|
+
return new Promise<Event>((resolve) => {
|
|
17
|
+
const handler = (event: Event) => {
|
|
18
|
+
eventTarget.removeEventListener(eventName, handler);
|
|
19
|
+
resolve(event);
|
|
20
|
+
};
|
|
21
|
+
eventTarget.addEventListener(eventName, handler);
|
|
22
|
+
});
|
|
23
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
// File Layout
|
|
5
|
+
// "rootDir": "./src",
|
|
6
|
+
// "outDir": "./dist",
|
|
7
|
+
|
|
8
|
+
// Environment Settings
|
|
9
|
+
// See also https://aka.ms/tsconfig/module
|
|
10
|
+
"module": "nodenext",
|
|
11
|
+
"target": "esnext",
|
|
12
|
+
// For nodejs:
|
|
13
|
+
"lib": ["esnext", "DOM"],
|
|
14
|
+
"types": ["node"],
|
|
15
|
+
// and npm install -D @types/node
|
|
16
|
+
|
|
17
|
+
// Other Outputs
|
|
18
|
+
"sourceMap": true,
|
|
19
|
+
"declaration": true,
|
|
20
|
+
"declarationMap": true,
|
|
21
|
+
|
|
22
|
+
// Stricter Typechecking Options
|
|
23
|
+
"noUncheckedIndexedAccess": true,
|
|
24
|
+
// "exactOptionalPropertyTypes": true,
|
|
25
|
+
|
|
26
|
+
// Style Options
|
|
27
|
+
// "noImplicitReturns": true,
|
|
28
|
+
// "noImplicitOverride": true,
|
|
29
|
+
// "noUnusedLocals": true,
|
|
30
|
+
// "noUnusedParameters": true,
|
|
31
|
+
// "noFallthroughCasesInSwitch": true,
|
|
32
|
+
// "noPropertyAccessFromIndexSignature": true,
|
|
33
|
+
|
|
34
|
+
// Recommended Options
|
|
35
|
+
"strict": true,
|
|
36
|
+
"jsx": "react-jsx",
|
|
37
|
+
"verbatimModuleSyntax": true,
|
|
38
|
+
"isolatedModules": true,
|
|
39
|
+
"noUncheckedSideEffectImports": true,
|
|
40
|
+
"moduleDetection": "force",
|
|
41
|
+
"skipLibCheck": true,
|
|
42
|
+
"outDir": "./dist",
|
|
43
|
+
"paths": {
|
|
44
|
+
"@/*": ["./src/*"],
|
|
45
|
+
"@result/sync": ["./src/result/sync/index.ts"],
|
|
46
|
+
"@result/async": ["./src/result/async/index.ts"],
|
|
47
|
+
"@result": ["./src/result/index.ts"],
|
|
48
|
+
"@utils/*": ["./src/utils/*"],
|
|
49
|
+
"@structures/*": ["./src/structures/*"],
|
|
50
|
+
"@structures/queue": ["./src/structures/queue/index.ts"],
|
|
51
|
+
"@internal/*": ["./src/internal/index.ts"],
|
|
52
|
+
"@dom": ["./src/dom/index.ts"]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"exclude": ["dist", "node_modules"]
|
|
56
|
+
}
|