reconstruct: autocraft algorithm; feature: rust-style result

reconstruct:
- move queue and sortedarray to dir datatype
- move semaphore and readwritelock to dir mutex
- reconstruct autocraft search algorithm, use hashmap instead of forloop
- adjust some code style
feature:
- add rust-style result lib
This commit is contained in:
2025-10-26 10:06:50 +08:00
parent ac70e1acd3
commit 119bc1997a
13 changed files with 1077 additions and 108 deletions

View File

@@ -4,7 +4,7 @@ import { createAccessControlCLI } from "./cli";
import { launchAccessControlTUI } from "./tui";
import * as peripheralManager from "../lib/PeripheralManager";
import { deepCopy } from "@/lib/common";
import { ReadWriteLock } from "@/lib/ReadWriteLock";
import { ReadWriteLock } from "@/lib/mutex/ReadWriteLock";
const args = [...$vararg];

View File

@@ -1,60 +1,62 @@
import { CraftManager } from "@/lib/CraftManager";
import * as peripheralManager from "../lib/PeripheralManager";
import { CCLog } from "@/lib/ccLog";
const log = new CCLog("autocraft.log");
const logger = new CCLog("autocraft.log");
const peripheralsRelativeSides = {
const peripheralsNames = {
packagesContainer: "minecraft:chest_10",
itemsContainer: "minecraft:chest_9",
packageExtractor: "create:packager_1",
blockReader: "front",
wiredModem: "back",
redstone: "front",
};
const packagesContainer = peripheral.wrap(
peripheralsNames.packagesContainer,
) as InventoryPeripheral;
const itemsContainer = peripheral.wrap(
peripheralsNames.itemsContainer,
) as InventoryPeripheral;
const packageExtractor = peripheral.wrap(
peripheralsNames.packageExtractor,
) as InventoryPeripheral;
const blockReader = peripheral.wrap(
peripheralsNames.blockReader,
) as BlockReaderPeripheral;
const wiredModem = peripheral.wrap(
peripheralsNames.wiredModem,
) as WiredModemPeripheral;
const turtleLocalName = wiredModem.getNameLocal();
enum State {
IDLE,
CHECK_PACK,
READ_RECIPE,
PULL_ITEMS,
CRAFT_OUTPUT,
}
function main() {
const packagesContainer = peripheralManager.findByNameRequired(
"inventory",
peripheralsRelativeSides.packagesContainer,
);
const itemsContainer = peripheralManager.findByNameRequired(
"inventory",
peripheralsRelativeSides.itemsContainer,
);
const packageExtractor = peripheralManager.findByNameRequired(
"inventory",
peripheralsRelativeSides.packageExtractor,
);
const blockReader = peripheralManager.findByNameRequired(
"blockReader",
peripheralsRelativeSides.blockReader,
);
const wiredModem = peripheralManager.findByNameRequired(
"wiredModem",
peripheralsRelativeSides.wiredModem,
);
const turtleLocalName = wiredModem.getNameLocal();
const craftManager = new CraftManager(turtleLocalName);
let hasPackage = redstone.getInput(peripheralsNames.redstone);
// let currentState = State.IDLE;
// let nextState = State.IDLE;
let hasPackage = redstone.getInput("front");
logger.info("AutoCraft init finished...");
while (true) {
if (!hasPackage) os.pullEvent("redstone");
hasPackage = redstone.getInput("front");
hasPackage = redstone.getInput(peripheralsNames.redstone);
if (!hasPackage) {
continue;
}
log.info(`Package detected`);
logger.info(`Package detected`);
const itemsInfo = packagesContainer.list();
for (const key in itemsInfo) {
const slot = parseInt(key);
const item = itemsInfo[slot];
log.info(`${item.count}x ${item.name} in slot ${key}`);
logger.info(`${item.count}x ${item.name} in slot ${key}`);
// Get package NBT
packagesContainer.pushItems(turtleLocalName, slot);
@@ -65,9 +67,9 @@ function main() {
const packageRecipes = CraftManager.getPackageRecipe(packageInfo);
// No recipe, just extract package
if (packageRecipes == undefined) {
if (packageRecipes.isNone()) {
packageExtractor.pullItems(turtleLocalName, 1);
log.info(`No recipe, just pass`);
logger.info(`No recipe, just pass`);
continue;
}
@@ -76,7 +78,7 @@ function main() {
packageExtractor.pullItems(turtleLocalName, 1);
// Pull and craft multi recipe
for (const recipe of packageRecipes) {
for (const recipe of packageRecipes.value) {
let craftOutputItem: BlockItemDetailData | undefined = undefined;
let restCraftCnt = recipe.Count;
@@ -84,15 +86,17 @@ function main() {
// Clear workbench
craftManager.pushAll(itemsContainer);
log.info(`Pull items according to a recipe`);
const craftCnt = craftManager.pullItems(
recipe,
itemsContainer,
restCraftCnt,
);
logger.info(`Pull items according to a recipe`);
const craftCnt = craftManager
.pullItems(recipe, itemsContainer, restCraftCnt)
.unwrapOrElse((error) => {
logger.error(error.message);
return 0;
});
if (craftCnt == 0) break;
craftManager.craft();
log.info(`Craft ${craftCnt} times`);
logger.info(`Craft ${craftCnt} times`);
restCraftCnt -= craftCnt;
// Get output item
@@ -101,9 +105,9 @@ function main() {
// Finally output
if (restCraftCnt > 0) {
log.warn(`Only craft ${recipe.Count - restCraftCnt} times`);
logger.warn(`Only craft ${recipe.Count - restCraftCnt} times`);
} else {
log.info(`Finish craft ${recipe.Count}x ${craftOutputItem?.id}`);
logger.info(`Finish craft ${recipe.Count}x ${craftOutputItem?.id}`);
}
craftManager.pushAll(itemsContainer);
}
@@ -114,7 +118,7 @@ function main() {
try {
main();
} catch (error: unknown) {
log.error(textutils.serialise(error as object));
logger.error(textutils.serialise(error as object));
} finally {
log.close();
logger.close();
}

View File

@@ -1,6 +1,5 @@
import { CCLog } from "./ccLog";
const log = new CCLog("CraftManager.log");
import { Queue } from "./datatype/Queue";
import { Result, Ok, Err, Option, Some, None } from "./thirdparty/ts-result-es";
// ComputerCraft Turtle inventory layout:
// 1, 2, 3, 4
@@ -70,6 +69,13 @@ interface CraftRecipe {
Count: number;
}
interface InventorySlotInfo {
name: string;
count: number;
maxCount: number;
slotNum: number;
}
type CraftMode = "keep" | "keepProduct" | "keepIngredient";
class CraftManager {
@@ -117,80 +123,122 @@ class CraftManager {
public static getPackageRecipe(
item: BlockItemDetailData,
): CraftRecipe[] | undefined {
): Option<CraftRecipe[]> {
if (
!item.id.includes("create:cardboard_package") ||
(item.tag as CreatePackageTag)?.Fragment?.OrderContext
?.OrderedCrafts?.[0] == undefined
) {
return undefined;
return None;
}
const orderedCraft = (item.tag as CreatePackageTag).Fragment.OrderContext
.OrderedCrafts;
return orderedCraft.map((value, _) => ({
PatternEntries: value.Pattern.Entries,
Count: value.Count,
}));
return new Some(
orderedCraft.map((value, _) => ({
PatternEntries: value.Pattern.Entries,
Count: value.Count,
})),
);
}
public pullItems(
recipe: CraftRecipe,
inventory: InventoryPeripheral,
limit: number,
): number {
let maxCraftCount = limit;
srcInventory: InventoryPeripheral,
craftCnt: number,
): Result<number> {
// Initialize hash map
const ingredientList = srcInventory.list();
const ingredientMap = new Map<string, Queue<InventorySlotInfo>>();
for (const key in ingredientList) {
const slotNum = parseInt(key);
const item = srcInventory.getItemDetail(slotNum)!;
if (ingredientMap.has(item.name)) {
ingredientMap.get(item.name)!.enqueue({
name: item.name,
slotNum: slotNum,
count: item.count,
maxCount: item.maxCount,
});
} else {
ingredientMap.set(
item.name,
new Queue<InventorySlotInfo>([
{
name: item.name,
slotNum: slotNum,
count: item.count,
maxCount: item.maxCount,
},
]),
);
}
}
let maxCraftCnt = craftCnt;
for (const index in recipe.PatternEntries) {
const entry = recipe.PatternEntries[index];
if (entry.Item.Count == 0 || entry.Item.id == "minecraft:air") {
continue;
}
const ingredientList = inventory.list();
let restCount = maxCraftCount;
for (const key in ingredientList) {
// Get item detail and check max count
const slot = parseInt(key);
const ingredient = inventory.getItemDetail(slot)!;
if (entry.Item.id != ingredient.name) {
continue;
if (!ingredientMap.has(entry.Item.id))
return new Err(Error(`No ingredient match ${entry.Item.id}`));
const ingredient = ingredientMap.get(entry.Item.id)!;
let restCraftCnt = maxCraftCnt;
while (restCraftCnt > 0 && ingredient.size() > 0) {
const slotItem = ingredient.dequeue()!;
// Check item max stack count
if (slotItem.maxCount < maxCraftCnt) {
maxCraftCnt = slotItem.maxCount;
restCraftCnt = maxCraftCnt;
}
const ingredientMaxCount = ingredient.maxCount;
if (maxCraftCount > ingredientMaxCount) {
maxCraftCount = ingredientMaxCount;
restCount = maxCraftCount;
}
log.info(
`Slot ${slot} ${ingredient.name} max count: ${ingredientMaxCount}`,
);
// TODO: Process multi count entry item
if (ingredient.count >= restCount) {
inventory.pushItems(
if (slotItem.count >= restCraftCnt) {
const pushItemsCnt = srcInventory.pushItems(
this.localName,
slot,
restCount,
CRAFT_SLOT_TABLE[parseInt(index) - 1],
slotItem.slotNum,
restCraftCnt,
CRAFT_SLOT_TABLE[index],
);
restCount = 0;
break;
if (pushItemsCnt !== restCraftCnt)
return new Err(
Error(
`Try to get items ${restCraftCnt}x "${slotItem.name}" from inventory, but only get ${pushItemsCnt}x`,
),
);
if (slotItem.count > restCraftCnt) {
ingredient.enqueue({
...slotItem,
count: slotItem.count - restCraftCnt,
});
}
restCraftCnt = 0;
} else {
inventory.pushItems(
const pushItemsCnt = srcInventory.pushItems(
this.localName,
slot,
ingredient.count,
CRAFT_SLOT_TABLE[parseInt(index) - 1],
slotItem.slotNum,
slotItem.count,
CRAFT_SLOT_TABLE[index],
);
restCount -= ingredient.count;
if (pushItemsCnt !== slotItem.count)
return new Err(
Error(
`Try to get items ${slotItem.count}x "${slotItem.name}" from inventory, but only get ${pushItemsCnt}x`,
),
);
restCraftCnt -= slotItem.count;
}
}
if (restCount > 0) return 0;
if (restCraftCnt > 0)
return new Err(Error("Not enough items in inventory"));
}
return maxCraftCount;
return new Ok(maxCraftCnt);
}
}

View File

@@ -30,7 +30,7 @@ export function concatSentence(words: string[], length: number): string[] {
* @see Source project, ts-deepcopy https://github.com/ykdr2017/ts-deepcopy
* @see Code pen https://codepen.io/erikvullings/pen/ejyBYg
*/
export const deepCopy = <T>(target: T): T => {
export function deepCopy<T>(target: T): T {
if (target === null) {
return target;
}
@@ -48,4 +48,4 @@ export const deepCopy = <T>(target: T): T => {
return cp as T;
}
return target;
};
}

View File

@@ -1,7 +1,7 @@
export class Node<T> {
public value: T;
public next?: Node<T>
public prev?: Node<T>
public next?: Node<T>;
public prev?: Node<T>;
constructor(value: T, next?: Node<T>, prev?: Node<T>) {
this.value = value;
@@ -15,10 +15,15 @@ export class Queue<T> {
private _tail?: Node<T>;
private _size: number;
constructor() {
constructor(data?: T[]) {
this._head = undefined;
this._tail = undefined;
this._size = 0;
if (data === undefined) return;
for (const item of data) {
this.enqueue(item);
}
}
public enqueue(data: T): void {
@@ -37,11 +42,11 @@ export class Queue<T> {
}
public dequeue(): T | undefined {
if (this._head === undefined) return undefined;
const node = this._head;
if (node === undefined) return undefined;
this._head = this._head!.next;
this._head!.prev = undefined;
this._head = node.next;
if (this._head !== undefined) this._head.prev = undefined;
this._size--;
return node.value;
@@ -68,8 +73,7 @@ export class Queue<T> {
const array: T[] = [];
let currentNode: Node<T> = this._head!;
for (let i = 0; i < this._size; i++) {
if (currentNode.value !== undefined)
array.push(currentNode.value);
if (currentNode.value !== undefined) array.push(currentNode.value);
currentNode = currentNode.next!;
}
@@ -82,13 +86,13 @@ export class Queue<T> {
return {
next(): IteratorResult<T> {
if (currentNode === undefined) {
return { value: undefined, done: true }
return { value: undefined, done: true };
} else {
const data = currentNode.value;
currentNode = currentNode.next;
return { value: data, done: false }
return { value: data, done: false };
}
}
}
},
};
}
}

View File

@@ -1,4 +1,4 @@
import { SortedArray } from "./SortedArray";
import { SortedArray } from "../datatype/SortedArray";
const E_CANCELED = new Error("Request canceled");
// const E_INSUFFICIENT_RESOURCES = new Error("Insufficient resources");

21
src/lib/thirdparty/ts-result-es/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 vultix
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,2 @@
export * from "./result";
export * from "./option";

View File

@@ -0,0 +1,343 @@
import { toString } from "./utils";
// import { Result, Ok, Err } from "./result";
interface BaseOption<T> extends Iterable<T> {
/** `true` when the Option is Some */
isSome(): this is SomeImpl<T>;
/** `true` when the Option is None */
isNone(): this is None;
/**
* Returns the contained `Some` value, if exists. Throws an error if not.
*
* If you know you're dealing with `Some` and the compiler knows it too (because you tested
* `isSome()` or `isNone()`) you should use `value` instead. While `Some`'s `expect()` and `value` will
* both return the same value using `value` is preferable because it makes it clear that
* there won't be an exception thrown on access.
*
* @param msg the message to throw if no Some value.
*/
expect(msg: string): T;
/**
* Returns the contained `Some` value.
* Because this function may throw, its use is generally discouraged.
* Instead, prefer to handle the `None` case explicitly.
*
* If you know you're dealing with `Some` and the compiler knows it too (because you tested
* `isSome()` or `isNone()`) you should use `value` instead. While `Some`'s `unwrap()` and `value` will
* both return the same value using `value` is preferable because it makes it clear that
* there won't be an exception thrown on access.
*
* Throws if the value is `None`.
*/
unwrap(): T;
/**
* Returns the contained `Some` value or a provided default.
*
* (This is the `unwrap_or` in rust)
*/
unwrapOr<T2>(val: T2): T | T2;
/**
* Returns the contained `Some` value or computes a value with a provided function.
*
* The function is called at most one time, only if needed.
*
* @example
* ```
* Some('OK').unwrapOrElse(
* () => { console.log('Called'); return 'UGH'; }
* ) // => 'OK', nothing printed
*
* None.unwrapOrElse(() => 'UGH') // => 'UGH'
* ```
*/
unwrapOrElse<T2>(f: () => T2): T | T2;
/**
* Calls `mapper` if the Option is `Some`, otherwise returns `None`.
* This function can be used for control flow based on `Option` values.
*/
andThen<T2>(mapper: (val: T) => Option<T2>): Option<T2>;
/**
* Maps an `Option<T>` to `Option<U>` by applying a function to a contained `Some` value,
* leaving a `None` value untouched.
*
* This function can be used to compose the Options of two functions.
*/
map<U>(mapper: (val: T) => U): Option<U>;
/**
* Maps an `Option<T>` to `Option<U>` by either converting `T` to `U` using `mapper` (in case
* of `Some`) or using the `default_` value (in case of `None`).
*
* If `default` is a result of a function call consider using `mapOrElse()` instead, it will
* only evaluate the function when needed.
*/
mapOr<U>(default_: U, mapper: (val: T) => U): U;
/**
* Maps an `Option<T>` to `Option<U>` by either converting `T` to `U` using `mapper` (in case
* of `Some`) or producing a default value using the `default` function (in case of `None`).
*/
mapOrElse<U>(default_: () => U, mapper: (val: T) => U): U;
/**
* Returns `Some()` if we have a value, otherwise returns `other`.
*
* `other` is evaluated eagerly. If `other` is a result of a function
* call try `orElse()` instead it evaluates the parameter lazily.
*
* @example
*
* Some(1).or(Some(2)) // => Some(1)
* None.or(Some(2)) // => Some(2)
*/
or(other: Option<T>): Option<T>;
/**
* Returns `Some()` if we have a value, otherwise returns the result
* of calling `other()`.
*
* `other()` is called *only* when needed.
*
* @example
*
* Some(1).orElse(() => Some(2)) // => Some(1)
* None.orElse(() => Some(2)) // => Some(2)
*/
orElse(other: () => Option<T>): Option<T>;
/**
* Maps an `Option<T>` to a `Result<T, E>`.
*/
// toResult<E>(error: E): Result<T, E>;
}
/**
* Contains the None value
*/
class NoneImpl implements BaseOption<never> {
isSome(): this is SomeImpl<never> {
return false;
}
isNone(): this is NoneImpl {
return true;
}
[Symbol.iterator](): Iterator<never, never, unknown> {
return {
next(): IteratorResult<never, never> {
return { done: true, value: undefined! };
},
};
}
unwrapOr<T2>(val: T2): T2 {
return val;
}
unwrapOrElse<T2>(f: () => T2): T2 {
return f();
}
expect(msg: string): never {
throw new Error(`${msg}`);
}
unwrap(): never {
throw new Error(`Tried to unwrap None`);
}
map(_mapper: unknown): None {
return this;
}
mapOr<T2>(default_: T2, _mapper: unknown): T2 {
return default_;
}
mapOrElse<U>(default_: () => U, _mapper: unknown): U {
return default_();
}
or<T>(other: Option<T>): Option<T> {
return other;
}
orElse<T>(other: () => Option<T>): Option<T> {
return other();
}
andThen(_op: unknown): None {
return this;
}
// toResult<E>(error: E): Err<E> {
// return Err(error);
// }
toString(): string {
return "None";
}
}
// Export None as a singleton, then freeze it so it can't be modified
export const None = new NoneImpl();
export type None = NoneImpl;
/**
* Contains the success value
*/
class SomeImpl<T> implements BaseOption<T> {
static readonly EMPTY = new SomeImpl<void>(undefined);
isSome(): this is SomeImpl<T> {
return true;
}
isNone(): this is NoneImpl {
return false;
}
readonly value!: T;
[Symbol.iterator](): Iterator<T> {
return [this.value][Symbol.iterator]();
}
constructor(val: T) {
if (!(this instanceof SomeImpl)) {
return new SomeImpl(val);
}
this.value = val;
}
unwrapOr(_val: unknown): T {
return this.value;
}
unwrapOrElse(_f: unknown): T {
return this.value;
}
expect(_msg: string): T {
return this.value;
}
unwrap(): T {
return this.value;
}
map<T2>(mapper: (val: T) => T2): Some<T2> {
return new Some(mapper(this.value));
}
mapOr<T2>(_default_: T2, mapper: (val: T) => T2): T2 {
return mapper(this.value);
}
mapOrElse<U>(_default_: () => U, mapper: (val: T) => U): U {
return mapper(this.value);
}
or(_other: Option<T>): Option<T> {
return this;
}
orElse(_other: () => Option<T>): Option<T> {
return this;
}
andThen<T2>(mapper: (val: T) => Option<T2>): Option<T2> {
return mapper(this.value);
}
// toResult<E>(_error: E): Ok<T> {
// return Ok(this.value);
// }
/**
* Returns the contained `Some` value, but never throws.
* Unlike `unwrap()`, this method doesn't throw and is only callable on an Some<T>
*
* Therefore, it can be used instead of `unwrap()` as a maintainability safeguard
* that will fail to compile if the type of the Option is later changed to a None that can actually occur.
*
* (this is the `into_Some()` in rust)
*/
safeUnwrap(): T {
return this.value;
}
toString(): string {
return `Some(${toString(this.value)})`;
}
}
// This allows Some to be callable - possible because of the es5 compilation target
// export const Some = SomeImpl as typeof SomeImpl & (<T>(val: T) => SomeImpl<T>);
export const Some = SomeImpl;
export type Some<T> = SomeImpl<T>;
export type Option<T> = Some<T> | None;
export type OptionSomeType<T extends Option<unknown>> =
T extends Some<infer U> ? U : never;
export type OptionSomeTypes<T extends Option<unknown>[]> = {
[key in keyof T]: T[key] extends Option<unknown>
? OptionSomeType<T[key]>
: never;
};
export namespace Option {
/**
* Parse a set of `Option`s, returning an array of all `Some` values.
* Short circuits with the first `None` found, if any
*/
export function all<T extends Option<any>[]>(
...options: T
): Option<OptionSomeTypes<T>> {
const someOption: unknown[] = [];
for (let option of options) {
if (option.isSome()) {
someOption.push(option.value);
} else {
return option as None;
}
}
return new Some(someOption as OptionSomeTypes<T>);
}
/**
* Parse a set of `Option`s, short-circuits when an input value is `Some`.
* If no `Some` is found, returns `None`.
*/
export function any<T extends Option<any>[]>(
...options: T
): Option<OptionSomeTypes<T>[number]> {
// short-circuits
for (const option of options) {
if (option.isSome()) {
return option as Some<OptionSomeTypes<T>[number]>;
} else {
continue;
}
}
// it must be None
return None;
}
export function isOption<T = any>(value: unknown): value is Option<T> {
return value instanceof Some || value === None;
}
}

View File

@@ -0,0 +1,536 @@
import { toString } from "./utils";
// import { Option, None, Some } from "./option";
/*
* Missing Rust Result type methods:
* pub fn contains<U>(&self, x: &U) -> bool
* pub fn contains_err<F>(&self, f: &F) -> bool
* pub fn and<U>(self, res: Result<U, E>) -> Result<U, E>
* pub fn expect_err(self, msg: &str) -> E
* pub fn unwrap_or_default(self) -> T
*/
interface BaseResult<T, E> extends Iterable<T> {
/** `true` when the result is Ok */
isOk(): this is OkImpl<T>;
/** `true` when the result is Err */
isErr(): this is ErrImpl<E>;
/**
* Returns the contained `Ok` value, if exists. Throws an error if not.
*
* The thrown error's
* [`cause'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause)
* is set to value contained in `Err`.
*
* If you know you're dealing with `Ok` and the compiler knows it too (because you tested
* `isOk()` or `isErr()`) you should use `value` instead. While `Ok`'s `expect()` and `value` will
* both return the same value using `value` is preferable because it makes it clear that
* there won't be an exception thrown on access.
*
* @param msg the message to throw if no Ok value.
*/
expect(msg: string): T;
/**
* Returns the contained `Err` value, if exists. Throws an error if not.
* @param msg the message to throw if no Err value.
*/
expectErr(msg: string): E;
/**
* Returns the contained `Ok` value.
* Because this function may throw, its use is generally discouraged.
* Instead, prefer to handle the `Err` case explicitly.
*
* If you know you're dealing with `Ok` and the compiler knows it too (because you tested
* `isOk()` or `isErr()`) you should use `value` instead. While `Ok`'s `unwrap()` and `value` will
* both return the same value using `value` is preferable because it makes it clear that
* there won't be an exception thrown on access.
*
* Throws if the value is an `Err`, with a message provided by the `Err`'s value and
* [`cause'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause)
* set to the value.
*/
unwrap(): T;
/**
* Returns the contained `Err` value.
* Because this function may throw, its use is generally discouraged.
* Instead, prefer to handle the `Ok` case explicitly and access the `error` property
* directly.
*
* Throws if the value is an `Ok`, with a message provided by the `Ok`'s value and
* [`cause'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause)
* set to the value.
*/
unwrapErr(): E;
/**
* Returns the contained `Ok` value or a provided default.
*
* @see unwrapOr
* @deprecated in favor of unwrapOr
*/
else<T2>(val: T2): T | T2;
/**
* Returns the contained `Ok` value or a provided default.
*
* (This is the `unwrap_or` in rust)
*/
unwrapOr<T2>(val: T2): T | T2;
/**
* Returns the contained `Ok` value or computes a value with a provided function.
*
* The function is called at most one time, only if needed.
*
* @example
* ```
* Ok('OK').unwrapOrElse(
* (error) => { console.log(`Called, got ${error}`); return 'UGH'; }
* ) // => 'OK', nothing printed
*
* Err('A03B').unwrapOrElse((error) => `UGH, got ${error}`) // => 'UGH, got A03B'
* ```
*/
unwrapOrElse<T2>(f: (error: E) => T2): T | T2;
/**
* Calls `mapper` if the result is `Ok`, otherwise returns the `Err` value of self.
* This function can be used for control flow based on `Result` values.
*/
andThen<T2, E2>(mapper: (val: T) => Result<T2, E2>): Result<T2, E | E2>;
/**
* Maps a `Result<T, E>` to `Result<U, E>` by applying a function to a contained `Ok` value,
* leaving an `Err` value untouched.
*
* This function can be used to compose the results of two functions.
*/
map<U>(mapper: (val: T) => U): Result<U, E>;
/**
* Maps a `Result<T, E>` to `Result<T, F>` by applying a function to a contained `Err` value,
* leaving an `Ok` value untouched.
*
* This function can be used to pass through a successful result while handling an error.
*/
mapErr<F>(mapper: (val: E) => F): Result<T, F>;
/**
* Maps a `Result<T, E>` to `Result<U, E>` by either converting `T` to `U` using `mapper`
* (in case of `Ok`) or using the `default_` value (in case of `Err`).
*
* If `default` is a result of a function call consider using `mapOrElse` instead, it will
* only evaluate the function when needed.
*/
mapOr<U>(default_: U, mapper: (val: T) => U): U;
/**
* Maps a `Result<T, E>` to `Result<U, E>` by either converting `T` to `U` using `mapper`
* (in case of `Ok`) or producing a default value using the `default` function (in case of
* `Err`).
*/
mapOrElse<U>(default_: (error: E) => U, mapper: (val: T) => U): U;
/**
* Returns `Ok()` if we have a value, otherwise returns `other`.
*
* `other` is evaluated eagerly. If `other` is a result of a function
* call try `orElse()` instead it evaluates the parameter lazily.
*
* @example
*
* Ok(1).or(Ok(2)) // => Ok(1)
* Err('error here').or(Ok(2)) // => Ok(2)
*/
or<E2>(other: Result<T, E2>): Result<T, E2>;
/**
* Returns `Ok()` if we have a value, otherwise returns the result
* of calling `other()`.
*
* `other()` is called *only* when needed and is passed the error value in a parameter.
*
* @example
*
* Ok(1).orElse(() => Ok(2)) // => Ok(1)
* Err('error').orElse(() => Ok(2)) // => Ok(2)
*/
orElse<T2, E2>(other: (error: E) => Result<T2, E2>): Result<T | T2, E2>;
/**
* Converts from `Result<T, E>` to `Option<T>`, discarding the error if any
*
* Similar to rust's `ok` method
*/
// toOption(): Option<T>;
}
/**
* Contains the error value
*/
export class ErrImpl<E> implements BaseResult<never, E> {
/** An empty Err */
static readonly EMPTY = new ErrImpl<void>(undefined);
isOk(): this is OkImpl<never> {
return false;
}
isErr(): this is ErrImpl<E> {
return true;
}
readonly error!: E;
private readonly _stack!: string;
[Symbol.iterator](): Iterator<never, never, unknown> {
return {
next(): IteratorResult<never, never> {
return { done: true, value: undefined! };
},
};
}
constructor(val: E) {
if (!(this instanceof ErrImpl)) {
return new ErrImpl(val);
}
this.error = val;
const stackLines = new Error().stack!.split("\n").slice(2);
if (
stackLines !== undefined &&
stackLines.length > 0 &&
stackLines[0].includes("ErrImpl")
) {
stackLines.shift();
}
this._stack = stackLines.join("\n");
}
/**
* @deprecated in favor of unwrapOr
* @see unwrapOr
*/
else<T2>(val: T2): T2 {
return val;
}
unwrapOr<T2>(val: T2): T2 {
return val;
}
unwrapOrElse<T2>(f: (error: E) => T2): T2 {
return f(this.error);
}
expect(msg: string): never {
// The cause casting required because of the current TS definition being overly restrictive
// (the definition says it has to be an Error while it can be anything).
// See https://github.com/microsoft/TypeScript/issues/45167
throw new Error(`${msg} - Error: ${toString(this.error)}\n${this._stack}`, {
cause: this.error as unknown,
});
}
expectErr(_msg: string): E {
return this.error;
}
unwrap(): never {
// The cause casting required because of the current TS definition being overly restrictive
// (the definition says it has to be an Error while it can be anything).
// See https://github.com/microsoft/TypeScript/issues/45167
throw new Error(
`Tried to unwrap Error: ${toString(this.error)}\n${this._stack}`,
{ cause: this.error as unknown },
);
}
unwrapErr(): E {
return this.error;
}
map(_mapper: unknown): Err<E> {
return this;
}
andThen<T2, E2>(_op: (val: never) => Result<T2, E2>): Result<T2, E | E2> {
return this;
}
mapErr<E2>(mapper: (err: E) => E2): Err<E2> {
return new Err(mapper(this.error));
}
mapOr<U>(default_: U, _mapper: unknown): U {
return default_;
}
mapOrElse<U>(default_: (error: E) => U, _mapper: unknown): U {
return default_(this.error);
}
or<T>(other: Ok<T>): Result<T, never>;
or<R extends Result<unknown, unknown>>(other: R): R;
or<T, E2>(other: Result<T, E2>): Result<T, E2> {
return other;
}
orElse<T2, E2>(other: (error: E) => Result<T2, E2>): Result<T2, E2> {
return other(this.error);
}
// toOption(): Option<never> {
// return None;
// }
toString(): string {
return `Err(${toString(this.error)})`;
}
get stack(): string | undefined {
return `${this.toString()}\n${this._stack}`;
}
}
// This allows Err to be callable - possible because of the es5 compilation target
// export const Err = ErrImpl as typeof ErrImpl & (<E>(err: E) => Err<E>);
export const Err = ErrImpl;
export type Err<E> = ErrImpl<E>;
/**
* Contains the success value
*/
export class OkImpl<T> implements BaseResult<T, never> {
static readonly EMPTY = new OkImpl<void>(undefined);
isOk(): this is OkImpl<T> {
return true;
}
isErr(): this is ErrImpl<never> {
return false;
}
readonly value!: T;
[Symbol.iterator](): Iterator<T> {
return [this.value][Symbol.iterator]();
}
constructor(val: T) {
if (!(this instanceof OkImpl)) {
return new OkImpl(val);
}
this.value = val;
}
/**
* @see unwrapOr
* @deprecated in favor of unwrapOr
*/
else(_val: unknown): T {
return this.value;
}
unwrapOr(_val: unknown): T {
return this.value;
}
unwrapOrElse(_f: unknown): T {
return this.value;
}
expect(_msg: string): T {
return this.value;
}
expectErr(msg: string): never {
throw new Error(msg);
}
unwrap(): T {
return this.value;
}
unwrapErr(): never {
// The cause casting required because of the current TS definition being overly restrictive
// (the definition says it has to be an Error while it can be anything).
// See https://github.com/microsoft/TypeScript/issues/45167
throw new Error(`Tried to unwrap Ok: ${toString(this.value)}`, {
cause: this.value as unknown,
});
}
map<T2>(mapper: (val: T) => T2): Ok<T2> {
return new Ok(mapper(this.value));
}
andThen<T2, E2>(mapper: (val: T) => Result<T2, E2>): Result<T2, E2> {
return mapper(this.value);
}
mapErr(_mapper: unknown): Ok<T> {
return this;
}
mapOr<U>(_default_: U, mapper: (val: T) => U): U {
return mapper(this.value);
}
mapOrElse<U>(_default_: (_error: never) => U, mapper: (val: T) => U): U {
return mapper(this.value);
}
or(_other: Result<T, unknown>): Ok<T> {
return this;
}
orElse<T2, E2>(_other: (error: never) => Result<T2, E2>): Result<T, never> {
return this;
}
// toOption(): Option<T> {
// return Some(this.value);
// }
/**
* Returns the contained `Ok` value, but never throws.
* Unlike `unwrap()`, this method doesn't throw and is only callable on an Ok<T>
*
* Therefore, it can be used instead of `unwrap()` as a maintainability safeguard
* that will fail to compile if the error type of the Result is later changed to an error that can actually occur.
*
* (this is the `into_ok()` in rust)
*/
safeUnwrap(): T {
return this.value;
}
toString(): string {
return `Ok(${toString(this.value)})`;
}
}
// This allows Ok to be callable - possible because of the es5 compilation target
// export const Ok = OkImpl as typeof OkImpl & (<T>(val: T) => Ok<T>);
export const Ok = OkImpl;
export type Ok<T> = OkImpl<T>;
export type Result<T, E = Error> = Ok<T> | Err<E>;
export type ResultOkType<T extends Result<unknown, unknown>> =
T extends Ok<infer U> ? U : never;
export type ResultErrType<T> = T extends Err<infer U> ? U : never;
export type ResultOkTypes<T extends Result<unknown, unknown>[]> = {
[key in keyof T]: T[key] extends Result<infer _U, unknown>
? ResultOkType<T[key]>
: never;
};
export type ResultErrTypes<T extends Result<unknown, unknown>[]> = {
[key in keyof T]: T[key] extends Result<infer _U, unknown>
? ResultErrType<T[key]>
: never;
};
export namespace Result {
/**
* Parse a set of `Result`s, returning an array of all `Ok` values.
* Short circuits with the first `Err` found, if any
*/
export function all<const T extends Result<any, any>[]>(
results: T,
): Result<ResultOkTypes<T>, ResultErrTypes<T>[number]> {
const okResult: unknown[] = [];
for (let result of results) {
if (result.isOk()) {
okResult.push(result.value);
} else {
return result as Err<ResultErrTypes<T>[number]>;
}
}
return new Ok(okResult as ResultOkTypes<T>);
}
/**
* Parse a set of `Result`s, short-circuits when an input value is `Ok`.
* If no `Ok` is found, returns an `Err` containing the collected error values
*/
export function any<const T extends Result<any, any>[]>(
results: T,
): Result<ResultOkTypes<T>[number], ResultErrTypes<T>> {
const errResult: unknown[] = [];
// short-circuits
for (const result of results) {
if (result.isOk()) {
return result as Ok<ResultOkTypes<T>[number]>;
} else {
errResult.push(result.error);
}
}
// it must be a Err
return new Err(errResult as ResultErrTypes<T>);
}
/**
* Wrap an operation that may throw an Error (`try-catch` style) into checked exception style
* @param op The operation function
*/
export function wrap<T, E = unknown>(op: () => T): Result<T, E> {
try {
return new Ok(op());
} catch (e) {
return new Err<E>(e as E);
}
}
/**
* Wrap an async operation that may throw an Error (`try-catch` style) into checked exception style
* @param op The operation function
*/
export function wrapAsync<T, E = unknown>(
op: () => Promise<T>,
): Promise<Result<T, E>> {
try {
return op()
.then((val) => new Ok(val))
.catch((e) => new Err(e));
} catch (e) {
return Promise.resolve(new Err(e as E));
}
}
/**
* Partitions a set of results, separating the `Ok` and `Err` values.
*/
export function partition<T extends Result<any, any>[]>(
results: T,
): [ResultOkTypes<T>, ResultErrTypes<T>] {
return results.reduce(
([oks, errors], v) =>
v.isOk()
? [[...oks, v.value] as ResultOkTypes<T>, errors]
: [oks, [...errors, v.error] as ResultErrTypes<T>],
[[], []] as [ResultOkTypes<T>, ResultErrTypes<T>],
);
}
export function isResult<T = any, E = any>(
val: unknown,
): val is Result<T, E> {
return val instanceof Err || val instanceof Ok;
}
}

View File

@@ -0,0 +1,11 @@
export function toString(val: unknown): string {
let value = String(val);
if (value === "[object Object]") {
try {
value = textutils.serialize(val as object);
} catch {
return "";
}
}
return value;
}