Created
April 7, 2021 06:03
-
-
Save Lucifier129/a47327cb02b7e3c15d8d634669eb0289 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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