Skip to content

Commit

Permalink
46
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Schaffer committed Mar 15, 2019
1 parent 6f3c7de commit 4de738b
Show file tree
Hide file tree
Showing 35 changed files with 801 additions and 108 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -61,6 +61,7 @@
"ts-node": "^7.0.1",
"tsconfig-paths": "^3.7.0",
"typedoc": "^0.14.2",
"typedoc-plugin-no-inherit": "^1.1.6",
"typescript": "^3.2.2",
"typescript-eslint-parser": "^21.0.2",
"uuid": "^3.3.2"
Expand Down
47 changes: 1 addition & 46 deletions packages/dandi/common/README.md
@@ -1,52 +1,7 @@
# @dandi/common

`@dandi/common` provides common types and utilities for the rest of the
`@dandi` system.
**Dandi** system.

`@dandi/common` does not have any dependencies on NodeJS, and therefore
can be used on classes shared with projects targeted for the web.

## Disposable

The `Disposable` interface allows implementing classes to define
behavior for cleaning up resources like IO streams, database
connections, as well as Observable and other event subscriptions.

When used with `@dandi/core`, `Disposable` instances are
automatically disposed by Dandi at the end of their lifecycle.

```typescript
class MyService implements Disposable {

constructor(private dbClient: DbClient) {}

public dispose(reason: string): void {
this.dbClient.release();
}

}
```

### Disposable Utilities

`Disposable` is also a static class provides several utility functions:

* **isDisposable(obj)** - Returns `true` if the object implements
`dispose()`; otherwise, `false`.

* **makeDisposable(obj, disposeFn)** - Modifies the specified object to
add the provided {@see DisposeFn} as the `Disposable.dispose`
implementation. If the object already has a function member named
`dispose`, it is wrapped and called before the new function.

* **use(obj, fn)** - Invokes the specified function in a `try`/`catch`,
statement, then `finally` disposes the object. Returns the value
returned by `fn`, or rethrows the error thrown by it.

* **useAsync(obj, fn)** - Same as `use`, but `fn` is invoked using `await`.

* **remapDisposed(target, reason)** - Overwrites members of the target
such that functions and property/field accessors throw an
`AlreadyDisposedError`, a read-only `disposed` property is set with
the value `true`, and the target is frozen (using `Object.freeze`) to
prevent further modification.
8 changes: 8 additions & 0 deletions packages/dandi/common/index.ts
@@ -1,3 +1,11 @@
/**
* @module
* [[include:README.md]]
*/

/**
*
*/
export * from './src/app-error'
export * from './src/async-mutex'
export * from './src/async-mutex-lock-already-released-error'
Expand Down
18 changes: 18 additions & 0 deletions packages/dandi/common/src/app-error.ts
@@ -1,12 +1,30 @@
/**
* A generic error that can be used directly or extended to be more specific. Includes an optional `innerError` property
* to allow retaining the original error (and its stack) when rethrowing more specific errors.
*/
export class AppError extends Error {

/**
* Builds an error stack from the specified `Error` instance, including the composite stack built from
* [[AppError.getStack]].
* @param err
*/
public static stack(err: Error): string {
return err instanceof AppError ? err.getStack() : err.stack
}

/**
*
* @param message The error message
* @param innerError The original error the caused the error being constructed
*/
constructor(message?: string, public readonly innerError?: Error) {
super(message)
}

/**
* Builds a composite stack from the instance's `stack` property and any nested `innerError` instances.
*/
public getStack(): string {
let stack = `${this.constructor.name} ${this.stack}`
if (this.innerError) {
Expand Down
@@ -1,5 +1,8 @@
import { AppError } from './app-error'

/**
* Thrown when attempting to access instance members of a locked resource when the lock has already been released.
*/
export class AsyncMutexLockAlreadyReleasedError extends AppError {
constructor() {
super('Cannot use this lock because it was already released')
Expand Down
63 changes: 63 additions & 0 deletions packages/dandi/common/src/async-mutex.ts
Expand Up @@ -4,6 +4,9 @@ import { globalSymbol } from './global.symbol'

const RELEASED = globalSymbol('AsyncMutex.RELEASED')

/**
* A reference to an object instance locked by [[AsyncMutex]]
*/
export type LockedObject<T> = T & Disposable

function lockedObject<T extends object>(obj: T, released: Promise<void>, dispose: () => void): LockedObject<T> {
Expand All @@ -28,8 +31,55 @@ function throwAlreadyReleased(): never {
const MUTEXES = new Map<any, AsyncMutex<any>>()
const DISPOSED_ERROR = 'The lock request was rejected because the underlying mutex was disposed'

/**
* A `mutex` (mutual exclusion object) implementation that allows controlled asynchronous access to a resource.
*
* Use [[AsyncMutex.for]] to create a "locked" version of a source. It is recommended that the resource is not made
* accessible through any means (instance properties, etc) to avoid accidental unlocked access.
*
* Only one [[AsyncMutex]] instance will be created per resource instance, ensuring that the underlying resource can be
* shared between multiple consuming instances without crossing the streams. As such, it is important that each
* consuming instance implements [[Disposable]] and calls [[AsyncMutex.dispose]] at the end if its lifecycle to avoid
* leaking the [[AsyncMutex]] instance.
*
* Note that resources that implement [[Disposable]] themselves will be automatically disposed when the [[AsyncMutex]]
* instance is disposed. Calling [[dispose]] on [[LockedObject]] instances generated by the mutex will _not_ dispose
* of the underlying resource.
*
* ```typescript
* class MutexConsumer implements Disposable {
*
* private lockedResource: AsyncMutex<ContentiousResource>
*
* constructor(myResource: ContentiousResource) {
* this.lockedResource = AsyncMutex.for(myResource)
* }
*
* public async doSomething(query: string): Promise<void> {
* await this.lockedResource.runLocked(async (resource: ContentiousResource) => {
* await resource.execute(query)
* })
* }
*
* public async dispose(reason: string): Promise<void> {
* await this.lockedResource.dispose(reason)
* Disposable.remapDisposed(this, reason)
* }
*
* }
* ```
*
* @typeparam T The type of locked resource
*/
export class AsyncMutex<T extends object> implements Disposable {

/**
* Constructs an [[AsyncMutex]] instance for the specified object. Only one [[AsyncMutex]] instance will be created
* per object, ensuring that `obj` can be shared between multiple consumers without crossing the streams.
*
* @typeparam T The type of object to lock
* @param obj The object to lock
*/
public static for<T extends object>(obj: T): AsyncMutex<T> {
let mutex = MUTEXES.get(obj)
if (!mutex) {
Expand All @@ -41,16 +91,29 @@ export class AsyncMutex<T extends object> implements Disposable {

private locks = []

/**
* @param lockObject The resource that will be locked
*/
private constructor(private lockObject: T) {
if (!this.lockObject) {
throw new Error('An lockObject must be specified')
}
}

/**
* Requests a lock using [[getLock]], then uses [[Disposable.useAsync]] with the resulting [[LockedObject]] to execute
* `fn`, and then release/dispose the lock.
* @param fn
*/
public async runLocked<TResult>(fn: (lock?: LockedObject<T>) => Promise<TResult>) {
return Disposable.useAsync(this.getLock(), fn)
}

/**
* Returns a promise that resolves to a [[LockedObject]] instance once there are no preceding lock requests in
* the queue. The fulfilled [[LockedObject]] instance must be disposed in order to release the lock. If `T`
* implements [[Disposable]], disposing [[LockedObject]] will _not_ dispose the underlying resource.
*/
public async getLock(): Promise<LockedObject<T>> {
let release: Function
const released = new Promise<void>(resolve => release = resolve)
Expand Down
6 changes: 5 additions & 1 deletion packages/dandi/common/src/call-site.ts
@@ -1,5 +1,9 @@
import CallSite = NodeJS.CallSite;
import CallSite = NodeJS.CallSite

/**
* @internal
* @ignore
*/
export function callsite(): CallSite[] {
const ogPrep = Error.prepareStackTrace
Error.prepareStackTrace = (_, stack) => stack
Expand Down
5 changes: 5 additions & 0 deletions packages/dandi/common/src/clone.ts
@@ -1,3 +1,8 @@
/**
* @internal
* @ignore
* @param obj
*/
export function cloneObject<T extends any>(obj: T): T {
if (!obj) {
return obj
Expand Down
9 changes: 8 additions & 1 deletion packages/dandi/common/src/constructor.ts
@@ -1,7 +1,14 @@
/**
* Represents a class constructor function.
*/
export interface Constructor<T = any> extends Function {
new (...args: any[]): T;
new (...args: any[]): T
}

/**
* Returns `true` if the specified object is a class constructor function; otherwise, `false`.
* @param obj
*/
export function isConstructor<T>(obj: any): obj is Constructor<T> {
if (typeof obj !== 'function') {
return false
Expand Down
3 changes: 3 additions & 0 deletions packages/dandi/common/src/currency.ts
Expand Up @@ -2,6 +2,9 @@ const CURRENCY_PATTERN = /^(\D+)?(\d+\.\d{0,2})$/

const values = new Map<string, Currency>()

/**
* @ignore
*/
export class Currency extends Number {
public static parse(value: string): Currency {
if (value === null || value === undefined) {
Expand Down
5 changes: 5 additions & 0 deletions packages/dandi/common/src/custom-inspector.d.ts
@@ -1 +1,6 @@
/**
* A `symbol` or `string` presenting the custom inspector for the current environment.
*
* The value will be the `util.inspect.custom` symbol for NodeJS, or `toString()` for browser environments.
*/
export const CUSTOM_INSPECTOR: string | symbol
4 changes: 2 additions & 2 deletions packages/dandi/common/src/custom-inspector.js
@@ -1,6 +1,6 @@
const { isBrowser } = require('./is-browser')
const { ENV_IS_BROWSER } = require('./env-is-browser')

if (isBrowser()) {
if (ENV_IS_BROWSER) {
module.exports.CUSTOM_INSPECTOR = 'toString'
} else {
module.exports.CUSTOM_INSPECTOR = require('util').inspect.custom
Expand Down
3 changes: 3 additions & 0 deletions packages/dandi/common/src/disposable-flags.ts
@@ -1,3 +1,6 @@
import { globalSymbol } from './global.symbol'

/**
* @ignore
*/
export const DISABLE_REMAP = globalSymbol('Disposable.DISABLE_REMAP')

0 comments on commit 4de738b

Please sign in to comment.