@effectstream/utils 0.100.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/package.json +28 -0
- package/src/binary-search.test.ts +38 -0
- package/src/binary-search.ts +45 -0
- package/src/captcha.ts +21 -0
- package/src/concurrency/condVar.ts +52 -0
- package/src/concurrency/latch.test.ts +51 -0
- package/src/concurrency/latch.ts +61 -0
- package/src/concurrency/mod.ts +4 -0
- package/src/concurrency/retry.test.ts +59 -0
- package/src/concurrency/retry.ts +67 -0
- package/src/concurrency/utils.ts +33 -0
- package/src/config.ts +484 -0
- package/src/constants.ts +10 -0
- package/src/decorators/bound.ts +22 -0
- package/src/decorators/mod.ts +1 -0
- package/src/mod.ts +8 -0
- package/src/runtime-spawn.ts +188 -0
- package/src/runtime.ts +89 -0
- package/src/types/json-query.ts +36 -0
- package/src/types/misc.ts +173 -0
- package/src/types/mod.ts +6 -0
- package/src/types/nominal.ts +168 -0
- package/src/types/typebox-helpers.ts +446 -0
- package/src/types/utils.ts +60 -0
- package/src/types/validators/algorand.ts +120 -0
- package/src/types/validators/cardano.ts +139 -0
- package/src/types/validators/mina.ts +54 -0
- package/src/types/validators/minicbor.ts +468 -0
- package/src/types/validators/mod.ts +2 -0
- package/src/types/validators/utils.ts +34 -0
- package/src/viem.ts +41 -0
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effectstream/utils",
|
|
3
|
+
"version": "0.100.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/mod.ts",
|
|
7
|
+
"./node-env": "./src/config.ts",
|
|
8
|
+
"./runtime": "./src/runtime.ts",
|
|
9
|
+
"./runtime-spawn": "./src/runtime-spawn.ts"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@coderspirit/nominal": "^4.1.1",
|
|
13
|
+
"@foxglove/crc": "^1.0.1",
|
|
14
|
+
"@sinclair/typebox": "0.34.41",
|
|
15
|
+
"@subsquid/ss58-codec": "^1.2.3",
|
|
16
|
+
"abitype": "1.1.0",
|
|
17
|
+
"bech32": "^2.0.0",
|
|
18
|
+
"bs58": "^6.0.0",
|
|
19
|
+
"bs58check": "^4.0.0",
|
|
20
|
+
"buffer": "^6.0.3",
|
|
21
|
+
"dotenv": "^16.4.7",
|
|
22
|
+
"effection": "^3.5.0",
|
|
23
|
+
"hi-base32": "^0.5.1",
|
|
24
|
+
"js-base64": "^3.7.7",
|
|
25
|
+
"js-sha512": "^0.9.0",
|
|
26
|
+
"viem": "2.37.3"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { binarySearch } from "./binary-search.ts";
|
|
3
|
+
|
|
4
|
+
test("binarySearch - finds exact match", () => {
|
|
5
|
+
const arr = [1, 3, 5, 7, 9];
|
|
6
|
+
const result = binarySearch(arr, 5, (val) => val);
|
|
7
|
+
expect(result).toEqual(2);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("binarySearch - finds closest value >= target", () => {
|
|
11
|
+
const arr = [1, 3, 5, 7, 9];
|
|
12
|
+
const result = binarySearch(arr, 6, (val) => val);
|
|
13
|
+
expect(result).toEqual(3);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("binarySearch - returns 0 if target is smaller than all elements", () => {
|
|
17
|
+
const arr = [1, 3, 5, 7, 9];
|
|
18
|
+
const result = binarySearch(arr, 0, (val) => val);
|
|
19
|
+
expect(result).toEqual(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("binarySearch - returns undefined if target is larger than all elements", () => {
|
|
23
|
+
const arr = [1, 3, 5, 7, 9];
|
|
24
|
+
const result = binarySearch(arr, 10, (val) => val);
|
|
25
|
+
expect(result).toEqual(undefined);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("binarySearch - returns undefined for empty array", () => {
|
|
29
|
+
const arr: number[] = [];
|
|
30
|
+
const result = binarySearch(arr, 5, (val) => val);
|
|
31
|
+
expect(result).toEqual(undefined);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("binarySearch - works with custom projection function", () => {
|
|
35
|
+
const arr = [{ id: 10 }, { id: 20 }, { id: 30 }];
|
|
36
|
+
const result = binarySearch(arr, 20, (val) => val.id);
|
|
37
|
+
expect(result).toEqual(1);
|
|
38
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the left-most entry index in an array satisfying the condition
|
|
3
|
+
*
|
|
4
|
+
* ex: binarySearch([6,7,7,8], 7, val => val) return `1`
|
|
5
|
+
*/
|
|
6
|
+
export function binarySearch<T>(
|
|
7
|
+
arr: T[],
|
|
8
|
+
target: number,
|
|
9
|
+
toNum: (val: T) => number,
|
|
10
|
+
): number | undefined {
|
|
11
|
+
let low = 0;
|
|
12
|
+
let high = arr.length - 1;
|
|
13
|
+
let result: number | undefined = undefined; // This will hold the index of the smallest value >= target, if it exists
|
|
14
|
+
|
|
15
|
+
if (arr.length === 0) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// if the target is smaller than all entries, just return right away
|
|
20
|
+
if (target - toNum(arr[0]) <= 0) {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
// if the target is bigger than all entries, just return right away
|
|
24
|
+
if (target - toNum(arr[arr.length - 1]) > 0) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// first: find the largest index where the value is smaller than the target
|
|
29
|
+
while (low < high) {
|
|
30
|
+
const mid = low + Math.floor((high - low) / 2);
|
|
31
|
+
|
|
32
|
+
const val = target - toNum(arr[mid]);
|
|
33
|
+
if (val <= 0) {
|
|
34
|
+
high = mid;
|
|
35
|
+
} else {
|
|
36
|
+
result = mid;
|
|
37
|
+
low = mid + 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// sanity check: should never happen
|
|
41
|
+
if (result == null) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return result + 1;
|
|
45
|
+
}
|
package/src/captcha.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// <reference lib="dom" />
|
|
2
|
+
// error[banned-triple-slash-directives]: triple slash directives that modify globals are not allowed
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This method will dynamically add:
|
|
6
|
+
* <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
|
|
7
|
+
*
|
|
8
|
+
* You can manually add this script to your HTML,
|
|
9
|
+
* this method is just a helper to do it programmatically.
|
|
10
|
+
*/
|
|
11
|
+
export async function injectReCaptchaToHTML(document: any, reCAPTCHA_site_key: string): Promise<void> {
|
|
12
|
+
const url =
|
|
13
|
+
`https://www.google.com/recaptcha/api.js?render=${reCAPTCHA_site_key}`;
|
|
14
|
+
return await new Promise((resolve, reject) => {
|
|
15
|
+
const el = document.createElement("script");
|
|
16
|
+
el.src = url;
|
|
17
|
+
el.addEventListener("load", () => resolve());
|
|
18
|
+
el.addEventListener("error", () => reject());
|
|
19
|
+
document.body.append(el);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Operation, Resolve, Scope, Task } from "effection";
|
|
2
|
+
import { action, useScope } from "effection";
|
|
3
|
+
|
|
4
|
+
export type CondVar<T> = {
|
|
5
|
+
wait: () => Operation<T>;
|
|
6
|
+
wake: (val: T) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Allows blocking execution until a specific wake is called
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```javascript
|
|
14
|
+
* const { wait, wake } = conditionVariable<void>();
|
|
15
|
+
* yield* spawn(function*() {
|
|
16
|
+
* console.log('start wait');
|
|
17
|
+
* yield* wait();
|
|
18
|
+
* console.log('done wait');
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* yield* sleep(1000);
|
|
22
|
+
* wait(); // allows the spawn to continue executing
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* See [Condition variables](https://en.wikipedia.org/wiki/Monitor_%28synchronization%29#Condition_variables) for more information
|
|
26
|
+
*/
|
|
27
|
+
export function conditionVariable<T>(): CondVar<T> {
|
|
28
|
+
let continuations: {
|
|
29
|
+
resolve: Resolve<T>;
|
|
30
|
+
scope: Scope;
|
|
31
|
+
}[] = [];
|
|
32
|
+
|
|
33
|
+
const wait = function* () {
|
|
34
|
+
const scope = yield* useScope();
|
|
35
|
+
return yield* action<T>(function* (resolve, _reject) {
|
|
36
|
+
continuations.push({ resolve, scope });
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const wake = (val: T): void => {
|
|
41
|
+
const oldVal = continuations;
|
|
42
|
+
continuations = [];
|
|
43
|
+
for (const continuation of oldVal) {
|
|
44
|
+
// note: we explicitly do not await here
|
|
45
|
+
void continuation.scope.run(function* () {
|
|
46
|
+
continuation.resolve(val);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return { wait, wake };
|
|
52
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { countdownLatch } from "./latch.ts";
|
|
3
|
+
import { run, sleep, spawn } from "effection";
|
|
4
|
+
|
|
5
|
+
test("countdownLatch - waits for countdown", async () => {
|
|
6
|
+
await run(function* () {
|
|
7
|
+
const { wait, countDown } = countdownLatch(2);
|
|
8
|
+
let done = false;
|
|
9
|
+
|
|
10
|
+
yield* spawn(function* () {
|
|
11
|
+
yield* wait();
|
|
12
|
+
done = true;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
yield* sleep(10);
|
|
16
|
+
expect(done).toEqual(false);
|
|
17
|
+
countDown();
|
|
18
|
+
|
|
19
|
+
yield* sleep(10);
|
|
20
|
+
expect(done).toEqual(false);
|
|
21
|
+
countDown();
|
|
22
|
+
|
|
23
|
+
yield* sleep(10);
|
|
24
|
+
expect(done).toEqual(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("countdownLatch - respects timeout", async () => {
|
|
29
|
+
await expect(async () => {
|
|
30
|
+
await run(function* () {
|
|
31
|
+
const { wait } = countdownLatch(1);
|
|
32
|
+
yield* wait(10); // 10ms timeout
|
|
33
|
+
});
|
|
34
|
+
}).toThrow("timeout");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("countdownLatch - no-op if already zero", async () => {
|
|
38
|
+
await run(function* () {
|
|
39
|
+
const { wait, countDown } = countdownLatch(1);
|
|
40
|
+
countDown();
|
|
41
|
+
countDown(); // Should not error
|
|
42
|
+
|
|
43
|
+
let done = false;
|
|
44
|
+
yield* spawn(function* () {
|
|
45
|
+
yield* wait();
|
|
46
|
+
done = true;
|
|
47
|
+
});
|
|
48
|
+
yield* sleep(10);
|
|
49
|
+
expect(done).toEqual(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Operation } from "effection";
|
|
2
|
+
import { action, race, sleep, spawn } from "effection";
|
|
3
|
+
import { conditionVariable } from "./condVar.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Allow blocking execution until counted down N times
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```javascript
|
|
10
|
+
* const { wait, countDown } = countdownLatch(2);
|
|
11
|
+
* yield* spawn(function* () {
|
|
12
|
+
* yield* wait();
|
|
13
|
+
* console.log("done");
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* yield* sleep(1000);
|
|
17
|
+
* countDown();
|
|
18
|
+
* yield* sleep(1000);
|
|
19
|
+
* countDown();
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* See [Barrier](https://en.wikipedia.org/wiki/Barrier_%28computer_science%29) for more information
|
|
23
|
+
*/
|
|
24
|
+
export function countdownLatch<T>(
|
|
25
|
+
count: number,
|
|
26
|
+
): {
|
|
27
|
+
wait: (timeout?: number) => Operation<void>;
|
|
28
|
+
countDown: () => void;
|
|
29
|
+
} {
|
|
30
|
+
const vars = new Array(count).fill(null).map(() => conditionVariable<void>());
|
|
31
|
+
let currIndex = 0;
|
|
32
|
+
|
|
33
|
+
const wait = (timeout?: number) =>
|
|
34
|
+
action<void>(function* (resolve, reject) {
|
|
35
|
+
// note: start at `currIndex` and not 0
|
|
36
|
+
// this is to support waiting on latches that have already started
|
|
37
|
+
const waitAll = yield* spawn(function* () {
|
|
38
|
+
for (let i = currIndex; i < count; i++) {
|
|
39
|
+
yield* vars[i].wait();
|
|
40
|
+
}
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
43
|
+
if (timeout == null) return yield* waitAll;
|
|
44
|
+
|
|
45
|
+
const rejectOnTimeout = yield* spawn(function* (): Operation<void> {
|
|
46
|
+
yield* sleep(timeout);
|
|
47
|
+
reject(new Error("timeout"));
|
|
48
|
+
});
|
|
49
|
+
yield* race([waitAll, rejectOnTimeout]);
|
|
50
|
+
});
|
|
51
|
+
const countDown = (): void => {
|
|
52
|
+
if (currIndex >= count) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const oldValue = currIndex;
|
|
56
|
+
currIndex++;
|
|
57
|
+
vars[oldValue].wake();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return { wait, countDown };
|
|
61
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { retry, tryYield } from "./retry.ts";
|
|
3
|
+
import { run } from "effection";
|
|
4
|
+
|
|
5
|
+
test("retry - succeeds immediately", async () => {
|
|
6
|
+
await run(function* () {
|
|
7
|
+
const result = yield* retry(
|
|
8
|
+
function* () { return "success"; },
|
|
9
|
+
(res) => res === "success"
|
|
10
|
+
);
|
|
11
|
+
expect(result).toEqual("success");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("retry - retries until success", async () => {
|
|
16
|
+
await run(function* () {
|
|
17
|
+
let attempts = 0;
|
|
18
|
+
const result = yield* retry(
|
|
19
|
+
function* () {
|
|
20
|
+
attempts++;
|
|
21
|
+
return attempts < 3 ? "fail" : "success";
|
|
22
|
+
},
|
|
23
|
+
(res) => res === "success"
|
|
24
|
+
);
|
|
25
|
+
expect(result).toEqual("success");
|
|
26
|
+
expect(attempts).toEqual(3);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("retry - fails after max attempts", async () => {
|
|
31
|
+
await expect(async () => {
|
|
32
|
+
await run(function* () {
|
|
33
|
+
yield* retry(
|
|
34
|
+
function* () { return "fail"; },
|
|
35
|
+
(res) => res === "success",
|
|
36
|
+
undefined,
|
|
37
|
+
2
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}).toThrow("Max attempts reached");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("tryYield - success case", async () => {
|
|
44
|
+
await run(function* () {
|
|
45
|
+
const op = function* () { return 42; };
|
|
46
|
+
const result = yield* tryYield(op());
|
|
47
|
+
expect(result.data).toEqual(42);
|
|
48
|
+
expect(result.error).toEqual(null);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("tryYield - failure case", async () => {
|
|
53
|
+
await run(function* () {
|
|
54
|
+
const op = function* () { throw new Error("fail"); };
|
|
55
|
+
const result = yield* tryYield(op());
|
|
56
|
+
expect(result.data).toEqual(null);
|
|
57
|
+
expect(result.error?.message).toEqual("fail");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Operation } from "effection";
|
|
2
|
+
|
|
3
|
+
export function* retry<Success, Failure = Success>(
|
|
4
|
+
fn: () => Operation<Success | Failure>,
|
|
5
|
+
isSuccess: (result: Success | Failure) => boolean,
|
|
6
|
+
onRetry?: (failResult: Failure) => Operation<void>,
|
|
7
|
+
maxAttempts?: number,
|
|
8
|
+
): Operation<Success> {
|
|
9
|
+
return yield* retryInternal<Success, Failure>(
|
|
10
|
+
fn,
|
|
11
|
+
isSuccess,
|
|
12
|
+
0,
|
|
13
|
+
onRetry,
|
|
14
|
+
maxAttempts,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
function* retryInternal<Success, Failure = Success>(
|
|
18
|
+
fn: () => Operation<Success | Failure>,
|
|
19
|
+
isSuccess: (result: Success | Failure) => boolean,
|
|
20
|
+
currAttempt: number,
|
|
21
|
+
onRetry?: (failResult: Failure) => Operation<void>,
|
|
22
|
+
maxAttempts?: number,
|
|
23
|
+
): Operation<Success> {
|
|
24
|
+
const result = yield* fn();
|
|
25
|
+
|
|
26
|
+
if (isSuccess(result)) {
|
|
27
|
+
return result as Success;
|
|
28
|
+
}
|
|
29
|
+
if (onRetry != null) {
|
|
30
|
+
yield* onRetry(result as Failure);
|
|
31
|
+
}
|
|
32
|
+
if (maxAttempts == null || currAttempt !== maxAttempts) {
|
|
33
|
+
return yield* retryInternal<Success, Failure>(
|
|
34
|
+
fn,
|
|
35
|
+
isSuccess,
|
|
36
|
+
currAttempt + 1,
|
|
37
|
+
onRetry,
|
|
38
|
+
maxAttempts,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
throw new Error("Max attempts reached");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Types for the result object with discriminated union
|
|
45
|
+
type Success<T> = {
|
|
46
|
+
data: T;
|
|
47
|
+
error: null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type Failure<E> = {
|
|
51
|
+
data: null;
|
|
52
|
+
error: E;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type Result<T, E = Error> = Success<T> | Failure<E>;
|
|
56
|
+
|
|
57
|
+
// Main wrapper function
|
|
58
|
+
export function* tryYield<T, E = Error>(
|
|
59
|
+
promise: Operation<T>,
|
|
60
|
+
): Operation<Result<T, E>> {
|
|
61
|
+
try {
|
|
62
|
+
const data = yield* promise;
|
|
63
|
+
return { data, error: null };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return { data: null, error: error as E };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Operation } from "effection";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Equivalent of Promise.then
|
|
5
|
+
*/
|
|
6
|
+
export function* then<A, B>(
|
|
7
|
+
operation: Operation<A>,
|
|
8
|
+
fn: (value: A) => Operation<B>,
|
|
9
|
+
): Operation<B> {
|
|
10
|
+
return yield* fn(yield* operation);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* TODO: this is a better `then` since it's much easier to chain
|
|
15
|
+
* @example
|
|
16
|
+
export function then<A, B>(
|
|
17
|
+
fn: (value: A) => Operation<B>,
|
|
18
|
+
): (operation: Operation<A>) => Operation<B> {
|
|
19
|
+
return function* (operation) {
|
|
20
|
+
return yield* fn(yield* operation);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
* but the problem is that it relies on a `pipe` function
|
|
24
|
+
* that is only available in effection v4
|
|
25
|
+
* end-goal: api looks like this
|
|
26
|
+
* @example
|
|
27
|
+
pipe(
|
|
28
|
+
operation,
|
|
29
|
+
then(lift((val) => val * 2)),
|
|
30
|
+
then(lift((val) => String(val)),
|
|
31
|
+
then(lift((val) => val.toUpperCase())),
|
|
32
|
+
)
|
|
33
|
+
*/
|