@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 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,4 @@
1
+ export * from "./condVar.ts";
2
+ export * from "./latch.ts";
3
+ export * from "./retry.ts";
4
+ export * from "./utils.ts";
@@ -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
+ */