Skip to content

Commit

Permalink
feat(contexts): introducing timedCancellable decorator and HOF and …
Browse files Browse the repository at this point in the history
…factored out common functionality in contexts domain
  • Loading branch information
CMCDragonkai committed Sep 12, 2022
1 parent d20fb50 commit eb4e287
Show file tree
Hide file tree
Showing 12 changed files with 1,983 additions and 314 deletions.
86 changes: 14 additions & 72 deletions src/contexts/decorators/cancellable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ContextCancellable } from '../types';
import { PromiseCancellable } from '@matrixai/async-cancellable';
import { setupCancellable } from '../functions/cancellable';
import * as contextsUtils from '../utils';

function cancellable(lazy: boolean = false) {
Expand All @@ -20,79 +20,21 @@ function cancellable(lazy: boolean = false) {
`\`${targetName}.${key.toString()}\` is not a function`,
);
}
const contextIndex = contextsUtils.contexts.get(target[key]);
if (contextIndex == null) {
throw new TypeError(
`\`${targetName}.${key.toString()}\` does not have a \`@context\` parameter decorator`,
);
}
descriptor['value'] = function (...params) {
let context: Partial<ContextCancellable> = params[contextIndex];
if (context === undefined) {
context = {};
params[contextIndex] = context;
const contextIndex = contextsUtils.getContextIndex(target, key, targetName);
descriptor['value'] = function (...args) {
let ctx: Partial<ContextCancellable> = args[contextIndex];
if (ctx === undefined) {
ctx = {};
args[contextIndex] = ctx;
}
// Runtime type check on the context parameter
if (typeof context !== 'object' || context === null) {
throw new TypeError(
`\`${targetName}.${key.toString()}\` decorated \`@context\` parameter is not a context object`,
);
}
if (
context.signal !== undefined &&
!(context.signal instanceof AbortSignal)
) {
throw new TypeError(
`\`${targetName}.${key.toString()}\` decorated \`@context\` parameter's \`signal\` property is not an instance of \`AbortSignal\``,
);
}
// Mutating the `context` parameter
if (context.signal === undefined) {
const abortController = new AbortController();
context.signal = abortController.signal;
const result = f.apply(this, params);
return new PromiseCancellable((resolve, reject, signal) => {
if (!lazy) {
signal.addEventListener('abort', () => {
reject(signal.reason);
});
}
void result.then(resolve, reject);
}, abortController);
} else {
// In this case, `context.signal` is set
// and we chain the upsteam signal to the downstream signal
const abortController = new AbortController();
const signalUpstream = context.signal;
const signalHandler = () => {
abortController.abort(signalUpstream.reason);
};
if (signalUpstream.aborted) {
abortController.abort(signalUpstream.reason);
} else {
signalUpstream.addEventListener('abort', signalHandler);
}
// Overwrite the signal property with this context's `AbortController.signal`
context.signal = abortController.signal;
const result = f.apply(this, params);
// The `abortController` must be shared in the `finally` clause
// to link up final promise's cancellation with the target
// function's signal
return new PromiseCancellable((resolve, reject, signal) => {
if (!lazy) {
if (signal.aborted) {
reject(signal.reason);
} else {
signal.addEventListener('abort', () => {
reject(signal.reason);
});
}
}
void result.then(resolve, reject);
}, abortController).finally(() => {
signalUpstream.removeEventListener('abort', signalHandler);
}, abortController);
}
contextsUtils.checkContextCancellable(ctx, key, targetName);
return setupCancellable(
(_, ...args) => f.apply(this, args),
lazy,
ctx,
args,
);
};
// Preserve the name
Object.defineProperty(descriptor['value'], 'name', {
Expand Down
214 changes: 46 additions & 168 deletions src/contexts/decorators/timed.ts
Original file line number Diff line number Diff line change
@@ -1,142 +1,9 @@
import type { ContextTimed } from '../types';
import { Timer } from '@matrixai/timer';
import { setupTimedContext } from '../functions/timed';
import * as contextsUtils from '../utils';
import * as contextsErrors from '../errors';
import * as utils from '../../utils';

/**
* This sets up the context
* This will mutate the `params` parameter
* It returns a teardown function to be called
* when the target function is finished
*/
function setupContext(
delay: number,
errorTimeoutConstructor: new () => Error,
targetName: string,
key: string | symbol,
contextIndex: number,
params: Array<any>,
): () => void {
let context: Partial<ContextTimed> = params[contextIndex];
if (context === undefined) {
context = {};
params[contextIndex] = context;
}
// Runtime type check on the context parameter
if (typeof context !== 'object' || context === null) {
throw new TypeError(
`\`${targetName}.${key.toString()}\` decorated \`@context\` parameter is not a context object`,
);
}
if (context.timer !== undefined && !(context.timer instanceof Timer)) {
throw new TypeError(
`\`${targetName}.${key.toString()}\` decorated \`@context\` parameter's \`timer\` property is not an instance of \`Timer\``,
);
}
if (
context.signal !== undefined &&
!(context.signal instanceof AbortSignal)
) {
throw new TypeError(
`\`${targetName}.${key.toString()}\` decorated \`@context\` parameter's \`signal\` property is not an instance of \`AbortSignal\``,
);
}
// There are 3 properties of timer and signal:
//
// A. If timer times out, signal is aborted
// B. If signal is aborted, timer is cancelled
// C. If timer is owned by the wrapper, then it must be cancelled when the target finishes
//
// There are 4 cases where the wrapper is used:
//
// 1. Nothing is inherited - A B C
// 2. Signal is inherited - A B C
// 3. Timer is inherited - A
// 4. Both signal and timer are inherited - A*
//
// Property B and C only applies to case 1 and 2 because the timer is owned
// by the wrapper and it is not inherited, if it is inherited, the caller may
// need to reuse the timer.
// In situation 4, there's a caveat for property A: it is assumed that the
// caller has already setup the property A relationship, therefore this
// wrapper will not re-setup this property A relationship.
if (context.timer === undefined && context.signal === undefined) {
const abortController = new AbortController();
const e = new errorTimeoutConstructor();
// Property A
const timer = new Timer(() => void abortController.abort(e), delay);
abortController.signal.addEventListener('abort', () => {
// Property B
timer.cancel();
});
context.signal = abortController.signal;
context.timer = timer;
return () => {
// Property C
timer.cancel();
};
} else if (
context.timer === undefined &&
context.signal instanceof AbortSignal
) {
const abortController = new AbortController();
const e = new errorTimeoutConstructor();
// Property A
const timer = new Timer(() => void abortController.abort(e), delay);
const signalUpstream = context.signal;
const signalHandler = () => {
// Property B
timer.cancel();
abortController.abort(signalUpstream.reason);
};
// If already aborted, abort target and cancel the timer
if (signalUpstream.aborted) {
// Property B
timer.cancel();
abortController.abort(signalUpstream.reason);
} else {
signalUpstream.addEventListener('abort', signalHandler);
}
// Overwrite the signal property with this context's `AbortController.signal`
context.signal = abortController.signal;
context.timer = timer;
return () => {
signalUpstream.removeEventListener('abort', signalHandler);
// Property C
timer.cancel();
};
} else if (context.timer instanceof Timer && context.signal === undefined) {
const abortController = new AbortController();
const e = new errorTimeoutConstructor();
let finished = false;
// If the timer resolves, then abort the target function
void context.timer.then(
(r: any, s: AbortSignal) => {
// If the timer is aborted after it resolves
// then don't bother aborting the target function
if (!finished && !s.aborted) {
// Property A
abortController.abort(e);
}
return r;
},
() => {
// Ignore any upstream cancellation
},
);
context.signal = abortController.signal;
return () => {
finished = true;
};
} else {
// In this case, `context.timer` and `context.signal` are both instances of
// `Timer` and `AbortSignal` respectively
// It is assumed that both the timer and signal are already hooked up to each other
return () => {};
}
}

/**
* Timed method decorator
*/
Expand All @@ -158,71 +25,82 @@ function timed(
`\`${targetName}.${key.toString()}\` is not a function`,
);
}
const contextIndex = contextsUtils.contexts.get(target[key]);
if (contextIndex == null) {
throw new TypeError(
`\`${targetName}.${key.toString()}\` does not have a \`@context\` parameter decorator`,
);
}
const contextIndex = contextsUtils.getContextIndex(target, key, targetName);
if (f instanceof utils.AsyncFunction) {
descriptor['value'] = async function (...params) {
const teardownContext = setupContext(
descriptor['value'] = async function (...args) {
let ctx: Partial<ContextTimed> = args[contextIndex];
if (ctx === undefined) {
ctx = {};
args[contextIndex] = ctx;
}
// Runtime type check on the context parameter
contextsUtils.checkContextTimed(ctx, key, targetName);
const teardownContext = setupTimedContext(
delay,
errorTimeoutConstructor,
targetName,
key,
contextIndex,
params,
ctx,
);
try {
return await f.apply(this, params);
return await f.apply(this, args);
} finally {
teardownContext();
}
};
} else if (f instanceof utils.GeneratorFunction) {
descriptor['value'] = function* (...params) {
const teardownContext = setupContext(
descriptor['value'] = function* (...args) {
let ctx: Partial<ContextTimed> = args[contextIndex];
if (ctx === undefined) {
ctx = {};
args[contextIndex] = ctx;
}
// Runtime type check on the context parameter
contextsUtils.checkContextTimed(ctx, key, targetName);
const teardownContext = setupTimedContext(
delay,
errorTimeoutConstructor,
targetName,
key,
contextIndex,
params,
ctx,
);
try {
return yield* f.apply(this, params);
return yield* f.apply(this, args);
} finally {
teardownContext();
}
};
} else if (f instanceof utils.AsyncGeneratorFunction) {
descriptor['value'] = async function* (...params) {
const teardownContext = setupContext(
descriptor['value'] = async function* (...args) {
let ctx: Partial<ContextTimed> = args[contextIndex];
if (ctx === undefined) {
ctx = {};
args[contextIndex] = ctx;
}
// Runtime type check on the context parameter
contextsUtils.checkContextTimed(ctx, key, targetName);
const teardownContext = setupTimedContext(
delay,
errorTimeoutConstructor,
targetName,
key,
contextIndex,
params,
ctx,
);
try {
return yield* f.apply(this, params);
return yield* f.apply(this, args);
} finally {
teardownContext();
}
};
} else {
descriptor['value'] = function (...params) {
const teardownContext = setupContext(
descriptor['value'] = function (...args) {
let ctx: Partial<ContextTimed> = args[contextIndex];
if (ctx === undefined) {
ctx = {};
args[contextIndex] = ctx;
}
// Runtime type check on the context parameter
contextsUtils.checkContextTimed(ctx, key, targetName);
const teardownContext = setupTimedContext(
delay,
errorTimeoutConstructor,
targetName,
key,
contextIndex,
params,
ctx,
);
const result = f.apply(this, params);
const result = f.apply(this, args);
if (utils.isPromiseLike(result)) {
return result.then(
(r) => {
Expand Down
Loading

0 comments on commit eb4e287

Please sign in to comment.