Skip to content

Instantly share code, notes, and snippets.

@Lucifier129
Created April 7, 2021 06:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lucifier129/a47327cb02b7e3c15d8d634669eb0289 to your computer and use it in GitHub Desktop.
Save Lucifier129/a47327cb02b7e3c15d8d634669eb0289 to your computer and use it in GitHub Desktop.
type ResourcesValue<T> = T extends []
? never
: T extends [Resource<infer V>]
? [V]
: T extends [Resource<infer V>, ...infer Rest]
? [V, ...ResourcesValue<Rest>]
: never;
type InitResource = {
init<T>(
onInit: TryResourceOptions<T>['onInit']
): TryResource<T> & FinallyResource<T, T>;
combine<T extends Resource<unknown>[]>(
...resources: T
): Resource<ResourcesValue<T>>;
};
type TryResource<T> = {
try<V>(
onValue: CatchResourceOptions<T, V>['onValue']
): CatchResource<T, V> & FinallyResource<T, V>;
};
type CatchResource<T, V> = {
catch(
onError: FinallyResourceOptions<T, V>['onError']
): FinallyResource<T, V>;
};
type FinallyResource<T, V> = {
finally(onFinally: CreateResourceOptions<T, V>['onFinally']): Resource<V>;
};
type Resource<V> = {
read(): Promise<V>;
dispose(): void;
};
const identity = <T>(x: T) => x;
const Resource: InitResource = {
init(onInit) {
return {
...tryResource({ onInit }),
...finallyResource({
onInit,
onValue: identity,
onError: throwError,
}),
};
},
combine<T extends Resource<unknown>[]>(
...resources: T
): Resource<ResourcesValue<T>> {
return {
async read() {
let values = resources.map((resource) => resource.read());
return Promise.all(values) as Promise<ResourcesValue<T>>;
},
dispose() {
for (let resource of resources) {
resource.dispose();
}
},
};
},
};
type TryResourceOptions<T> = {
onInit: () => Promise<T> | T;
};
const throwError = (error: Error) => {
throw error;
};
const tryResource = <T>({ onInit }: TryResourceOptions<T>): TryResource<T> => {
return {
try(onValue) {
return {
...catchResource({ onInit, onValue }),
...finallyResource({
onInit,
onValue,
onError: throwError,
}),
};
},
};
};
type CatchResourceOptions<T, V> = TryResourceOptions<T> & {
onValue: (value: T) => Promise<V> | V;
};
const catchResource = <T, V>({
onInit,
onValue,
}: CatchResourceOptions<T, V>): CatchResource<T, V> => {
return {
catch(onError) {
return finallyResource({ onInit, onValue, onError });
},
};
};
type FinallyResourceOptions<T, V> = CatchResourceOptions<T, V> & {
onError: (error: Error) => Promise<V> | V;
};
const finallyResource = <T, V>({
onInit,
onValue,
onError,
}: FinallyResourceOptions<T, V>): FinallyResource<T, V> => {
return {
finally(onFinally) {
return createResource({ onInit, onValue, onError, onFinally });
},
};
};
type CreateResourceOptions<T, V> = FinallyResourceOptions<T, V> & {
onFinally: (value: T) => unknown;
};
const createResource = <T, V>({
onInit,
onValue,
onError,
onFinally,
}: CreateResourceOptions<T, V>): Resource<V> => {
let hasRun = false;
let value: T | undefined;
let resource: Resource<V> = {
async read(this: Resource<V>) {
this.dispose();
try {
value = await onInit();
hasRun = true;
return await onValue(value);
} catch (error) {
if (error instanceof Error) {
return await onError(error);
}
throw error;
}
},
dispose() {
if (hasRun) {
hasRun = false;
onFinally(value!);
}
},
};
return resource;
};
type FS = {
open(
filename: string,
mode: string
): {
readFile(charset: string): Promise<string>;
writeFile(content: string): Promise<void>;
close(): void;
closed(): boolean;
};
};
const fs: FS = {
open(filename, mode) {
let isClosed = false;
return {
async readFile(charset) {
console.log('readFile', { filename, mode, charset })
return `readFile: ${JSON.stringify({ filename, mode, charset })}`;
},
async writeFile(content) {
console.log('writeFile', {
filename,
mode,
content,
});
},
close() {
isClosed = true;
},
closed() {
return isClosed;
},
};
},
};
async function main() {
let resource0 = Resource.init(() => fs.open('/path/to/file', 'r+'))
.try((handle) => handle.readFile('utf-8'))
.finally((handle) => {
console.log('close resource0');
handle.close();
});
let resource1 = Resource.init(() => fs.open('/path/to/file', 'w+')).finally(
(handle) => {
console.log('close resource1');
handle.close();
}
);
let combinedResource = Resource.combine(resource0, resource1);
try {
let [content, handle1] = await combinedResource.read();
// uncomment to see the difference
// throw new Error('stop!')
handle1.writeFile(content);
} finally {
combinedResource.dispose();
}
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment