remoter
v1.1.0
Published
Remotely resolveable Native Javascript Promise that exposes it's resolver and rejector callbacks and a debugging API with lifecycle events
Downloads
6
Maintainers
Readme
Remoter
Remoter is a remotely resolveable native Javascript Promise that exposes it's resolver and rejector callbacks. It is supposed to help you to keep a somewhat consistent code style when using asyc
/await
syntax with functions that have a callback API. It also helps you in debugging Promises and can act as a plug-in replacement for the native Promise. Additionally, it quite lightweight and has zero dependencies.
In a nutshell
// Write this:
function writeFileAsync (fileName, data, options) {
const { promise, callback } = new Remoter;
fs.writeFile(fileName, data, options, callback);
return promise;
}
// Instead of this:
function writeFileAsync (fileName, data, options) {
return new Promise(
(resolve, reject) => {
fs.writeFile(
fileName,
data,
options,
(error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
}
);
}
);
}
It's a Promise
Remoter is not only a then-able, it's an extension of the native Promise.
Where is it useful?
- When you need to promisify a callback-based API like
fs
, anything with a callback with error and result arguments, the other way around, two callbacks (e.g. onError, onSuccess), or something odd - When you want to use
await
to wait for events e.g.EventEmitter.on(...)
orawait sleep(...)
- When implementing an abortable/cancelable
Promise
- When your code looks like a funny mix of
await
,new Promise
, and() => 'Arrow Functions'
or looks like a callback hell - When you generally want to reshape control flow
- When you want a piece of asynchronous code to wait for an external event to happen
- When the promise receiving the data is not directly correlated with the
Promise
returning the value - When you woder if the cat is dead or alive but you don't want to open the box
- When one of your Promises does not have the value it should have or you suspect it to be settled more than once
- Other stuff that is totally up to your creativity! (Please let me know 🙏)
Installation
npm install remoter
Usage
Quick Start
require
const Remoter = require('remoter');
const remoter = new Remoter;
remoter.then(
(value) => console.log(value)
);
remoter.resolve('The answer is 42');
or if you want to be really cool:
const Remoter = require('remoter');
const {remoter, resolve, reject} = new Remoter;
remoter.then(
value => console.log(value)
).catch(
error => console.log(error)
);
resolve('The answer is 42');
import
import * as Remoter from 'remoter';
const remoter = new Remoter;
remoter.then(
(value) => console.log(value)
);
remoter.resolve('The answer is 42');
Plug-in replacement
Besides being used instead of the native Promise, Remoter can be used as a plug-in replacement for the native Promise to allow for debugging and lifecycle monitoring:
// Don't do this in Production!
const Remoter = require('remoter');
const NativePromise = Promise;
let id = 0;
Remoter.on(
'create',
remoter => {
const instanceId = ++id;
console.log(`${instanceId}: Remoter created`);
remoter.on(
'*',
(...args) =>
console.log(`${instanceId}:`, ...args)
)
}
);
// You might want to disable Remoter.instanceArgument if your claiming callbacks
// in .then, .catch and .finally accept more than one argument 🙈
Remoter.instanceArgument = false;
// You also might want to disable .finallyArgument if your claiming .finally
// callbacks accept arguments 🙉
Remoter.finallyArgument = false;
Promise = Remoter;
//...
const promise = new Promise(
(resolve, reject) => {
//...
resolve(true); // <someId>: resolved true
reject('uh oh 😯'); // <someId>: oversaturated uh oh 😯
}
); // <someId>: Remoter created
The necesarry singletons, properties, EventEmitters, and WeakMaps are created the moment you call the .on
hook the first time. They are destroyed as soon as you .off
all events. This is true for the introspection events on the Remoter class and the introspection events on the respective remoter instance individually. Each remoter instance holds its own introspection instances, meaning you can introspect a single Promise without bloating the others. This also means you don't need to use the class event hook if you are only looking at a specific Promise.
The native Promise class is available via Remoter.Promise
.
Please don't use this functionality to make Promises that can be fulfilled more than once.
Why?
- Promises are meant to be settled only once. You will buy into more problems than you wanted to overcome. Promised. Use an EventEmitter instead:
const EventEmitter = require('events');
. - It won't work as sleak as you expect as you would need to use the
.on
hook and a.catch
callback instead of the.then
,.catch
, or alternatively.finally
callbacks and end up with an EventEmitter again. If you want toawait
events, there is an example here that shows you how to do that. - The introspection API of Remoter uses additional resources, including lightweight EventEmitters (that add to your event loop) and WeakMaps (that use your heap).
API
Other than the Remoter-specific API mentioned here, the API is meant to be identical to the native Promise API.
Create a new instance
In addition to the native Promise constructor
const remoter = new Remoter(
(resolve, reject) => {
...
}
);
the Remoter can be instanciated without an executor:
const remoter = new Remoter;
Settling (resolving and rejecting) the Remoter instance from the outsite
.resolve([value])
Fulfills or resolves the remoter instance with the given value. value can be a value or another remoter instance, native promise or thenable. Returns the remoter instance.
const remoter = new Remoter;
remoter.then(console.log); // Will output: 42 Remoter [Promise] { 42 }
remoter.resolve(42);
.fulfill([value])
Fulfills the the remoter instance. Alias for .resolve([value])
.
.reject([error])
Rejects the remoter instance with the given error. Returns the remoter instance.
const remoter = new Remoter;
remoter.catch(console.log); // Will output: Question not found. Remoter [Promise] { <rejected> 'Question not found.' }
remoter.reject('Question not found.');
Interoperability with the native Promise
Remoters and Promises are meant to be exchangeable when used with other API's and with each other.
Exchangeable
In general, as Remoter is derived from Promise, it can be handed to API's that expect a native Promise or any other known then-able implementation. However, in some cases where the receiving codes uses a constructor indentity check instead of traversing the instance tree, the remoter instance cannot be used directly (as any other .thenable or Promise derivative also can't be used).
||Detection Method|Remark|
|-|-|-|
|✔|promise instanceof Promise
||
|✔|promise.then != undefined
or promise.then instanceof Function
||
|(✔)|Object.prototype.toString.call(promise) == '[object Promise]'
|This might change in the future! |
|❌|promise.constructor === Promise
or similar|Hand over remoter.promise
or Promise.resolve(remoter)
instead|
Remoter can also be used in places where a library that generates promises has a configuration slot for the promise implementation that it uses to generate the promises it returns.
Chaining
Remoters can be chained like native Promises.
const Remoter = require('remoter');
const remoter = new Remoter;
remoter.then(
value => value+29
).then(
value => value+12
).then(
value => console.log(value) // Will output: 42
);
remoter.resolve(1);
const Remoter = require('remoter');
const outerRemoter = new Remoter;
const innerRemoter = new Remoter;
outerRemoter.then(
value => console.log(value) // Will output: 42
);
outerRemoter.resolve(innerRemoter);
innerRemoter.resolve(42);
Composition
For Composition, Remoters are meant to be fully compatible to Promises and vice versa:
|Chain call|Result||
|----------|------|-|
|Remoter.resolve(promise)
|new Remoter|The remoter instance is resolved when the promise resolves. Keep in mind that when settling the remoter instance from the outsite using .resolve([value])
or .reject([error])
the promise will oversaturate immediately when one the of the (resolve, reject)
callbacks has been called before or will oversaturate in the moment when one of those callbacks is invoked after outside settling via .resolve([value])
or .reject([error])
.|
|Remoter.resolve(remoter)
|new Remoter|The inner remoter instance can settle from its executor calling one of the (resolve, reject)
callbacks and from the outside using .resolve([value])
or .reject([error])
. The newly created remoter instance will settle in chain with the inner remoter or from the outside using .resolve([value])
or .reject([error])
. The .remote
settling property of the outer remoter instance is tied to the inner remoter's remote settling property.|
|Remoter.resolve(thenable)
|new Remoter|See above Remoter.resolve(promise)
.|
|Promise.resolve(remoter)
|new Promise|As the outer Promise has no executor the settling of the outer Promise is completely dependent on the inner remoter instance which can be settled either through its executor's (resolve, reject)
callbacks or its .resolve([value])
or .reject([error])
methods. To obtain a persistent promise instance from the remoter use the .promise
property of the remoter instance instead.|
Remoter lifecycle properties
Result-independent Promise status properties
.pending
Read-only property indicating that the Remoter is pending, meaning it has not been fulfilled or rejected yet, or, if it follows another Remoter, Promise or Thenable, same has not been settled yet. It is true
if either .fulfilled is false
or .rejected is false
. Otherwise it is true
.
.settled
Read-only property indicating if the Promise is settled, meaning it has either been fulfilled or rejected. It is true
if .fulfilled is true
or .rejected is true
, but if the Remoter follows another Remoter, Promise or Thenable, same is not fulfilled yet, otherwise it is false
. See '.resolved' for including chaining or composition fates. You normally want to use this property to see if your Remoter has a value ready!.
.resolved
Read-only property that is true
if the Promise is either fulfilled or rejected or it followes another Remoter, Promise, or Thenable. It is true
if either .fulfilled or .rejected is true
(meaning .pending
is false
) or it follows another Remoter, Promise or Thenable throgh chaining or composition. You normally want to use .settled
to see if your Remoter has a value ready!.
.oversaturated
Read-only property indicating if there was an attempt to settle the promise more than once. While it is possible to call a reject or resolve callback of a Promise multiple times, the respective settling callbacks get only called once. This property allows for introspection if that happend:
const Remoter = require('remoter');
function oversaturationLog () {
if (!this.oversaturated)
console.log(`I feel fulfilled 😊`);
else
console.log(`I got too much 🤢`);
}
const remoter = new Remoter(
(resolve, reject) => {
setTimeout(
reject,
1e3,
new Error('The cat is a dog ideed')
);
resolve();
}
);
remoter.finally(oversaturationLog); // I feel fulfilled 😊
setTimeout(
oversaturationLog.bind(remoter), // I got too much 🤢
1.5e3
);
If you suspect one of your Promises to be settled more than once, there is a lifecycle event that helps you in finding out.
Result-dependent Promise status properties
Remoter offers read-only properties to expose its state to code outside the executor and settling callbacks. Those are set right before the settling callbacks attached via .then, .catch, and .finally are invoked.
.fulfilled
Read-only property that is false
while the Remoter is pending. If the Remoter follows another Remoter, Promise or Thenable, it will turn true
when that Remoter, Promise or Thenable is fulfulled. Returns true
from the moment right before the user-defined resolver is executed (if there is one defined).
.rejected
Read-only property that is false
while the Remoter is pending. If the Remoter follows another Remoter, Promise or Thenable, it will turn true
when that Remoter, Promise or Thenable is rejected. Returns true
from the moment right before the user-defined rejector is executed (if there is one defined).
Remote settling state
.remote
Read-only property that is true
if the remoter instance has been settled outside of its executor using .resolve([value]) or rejecte([error]), otherwise false
. While pending, the property is null
;
const Remoter = require('remoter');
const resolvedFromOutside = new Remoter;
resolvedFromOutside.resolve();
resolvedFromOutside.then(
function () {
console.log(this.remote); // Will output: true
}
);
const Remoter = require('remoter');
const resolvedInExecutor = new Remoter(
resolve => resolve()
);
resolvedInExecutor.then(
function () {
console.log(this.remote); // Will output: false
}
);
const Remoter = require('remoter');
const resolvedRightAway = Remoter.resolve();
resolvedRightAway.then(
function () {
console.log(this.remote); // Will output: false
}
);
Result and remote status sugar
For your convenience Remoter provides read-only sugar properties to detect all combinations of settling status and remote status of a remoter instance.
.resolvedRemotely
Read-only property that is true
when the Remoter has been resolved using .resolve([value]) or the remoter follows another Remoter, Promise or Thenable that has been fulfilled or rejected, otherwise false
. It is sugar for remoter.remote && remoter.resolved
.
.settledRemotely
Read-only property that is true
when the Remoter has been settled using .resolve([value]), rejected .reject([error]) or the remoter follows another Remoter, Promise or Thenable that has been rejected, otherwise false
. It will always be true
when .remote
is true
. It is sugar for remoter.remote && remoter.settled
.
.fulfilledRemotely
Read-only property that is true
when the Remoter has been fulfilled using .fulfill([value]), .resolve([value]) or the remoter follows another Remoter, Promise or Thenable that has been fulfilled, otherwise false
. It is sugar for remoter.remote && remoter.fulfilled
.
.rejectedRemotely
Read-only property that is true
when the Remoter has been rejected using .reject([error]) or the remoter follows another Remoter, Promise or Thenable that has been rejected, otherwise false
. It is sugar for remoter.remote && remoter.rejected
.
Result-handling status properties
Remoter offers status properties to determine if the value or error of a settled Promise has already been delivered to a callback registered via .then, .catch, or .finally at least once. See also lifecycle events.
.claimed
Read-only property that is true
when the Promise has been resolved and its value has already been delivered to a callback registered via .then, otherwise false
. See also lifecycle events.
.caught
Read-only property that is true
when the Promise has been rejected and its error has already been delivered to a callback registered via .catch, otherwise false
. See also lifecycle events.
.finalized
Read-only property that is true
when the Promise has been resolved or rejected and its value or error has already been delivered to a callback registered via .finally, otherwise false
. See also lifecycle events.
Claim settled Remoter results
Claiming the settled results of the Remoter works exactly like claiming settled results from a Promise. However, there is one slight difference for your convenience. For named fuctions and anonymous functions (functions that can have their own this
context), the this
context is the remoter instance:
const remoter = new Remoter;
remoter.then(
function (value) {
console.log(`Remoter resolved ${this.remote?'remotely ':''}with value`, value);
// Will output: Remoter resolved remotely with value 42
}
);
remoter.resolve(42);
To circumvent this behavior you can use a bound function with a custom this
context or use an arrow function. Those callbacks are invoked with an additonal argument containing a reference to the remoter instance:
const remoter = new Remoter;
remoter.then(
(value, remoterInstance) => {
console.log(`Remoter resolved ${remoterInstance.remote?'remotely ':''}with value`, value);
// Will output: Remoter resolved remotely with value 42
}
);
const thenCallback = function (value) {
console.log(`Remoter resolved ${remoter.remote?'remotely ':''}with value`, value);
// Will output: Remoter resolved remotely with value 42
}
remoter.then(
thenCallback.bind(this)
);
remoter.resolve(42);
Remoter uses the prototype attribute of the callback to determine which behavior to employ. If the prototype is undefined
, the 2nd argument option is employed instead of a bound this
context pointing to the remoter instance. Keep in mind that some loggers and console.log are bound functions and native functions might lack a prototype property. In those cases, Remoter will pass the remoter instance alongside the value as an additional (in this exampe second) argument to the callback. To overcome this behavior you can wrap the function into an anonymous function, e.g. remoter.then(function (targetLength) { return 'silly'.pad(targetLength) });
or turn off this behavior using the .instanceArgument
setting.
.then(thenCallback[, catchCallback])
Identical to the native Promise .then
method. When lifecycle introspection callbacks are attached to the remoter instance additionally emits then
event with thenCallback as payload argument. If catchCallback is given, also emits catch
event with catchCallback as payload argument. Except, when thenCallback and catchCallback are identical, emits finally
event with thenCallback as payload argument.
If possible, Remoter automatically sets the this
context of the callback to the remoter instance. See Claim settled Remoter results and .instanceArgument
for more information.
.catch(catchCallback)
Identical to the native Promise .catch
method. When lifecycle introspection callbacks are attached to the remoter instance additionally emits catch
event with catchCallback as payload argument.
If possible, Remoter automatically sets the this
context of the callback to the remoter instance. See Claim settled Remoter results and .instanceArgument
for more information.
.finally(finallyCallback)
Identical to the native Promise .finally
method. When lifecycle introspection callbacks are attached to the remoter instance additionally emits finally
event with finallyCallback as payload argument.
If possible, Remoter automatically sets the this
context of the callback to the remoter instance. See Claim settled Remoter results and .instanceArgument
for more information.
In contrast to the native Promise, callbacks registered using .finally
are passed an errorOrValue argument. This functionality can be turned off for the remoter instance either by wrapping the callback in a function, e.g. remoter.finally(onlyOneArgument => yourCallback(onlyOneArgument))
, by setting the .finallyArgument
property to false or by setting the Remoter.finallyArgument
global setting to false.
.finallyArgument
.finallyArgument
is a boolean property, that when set to false prevents passing of an errorOrValue argument to callbacks registered via .finally
. When set to true, enables the this behavior. This setting overrides the corresponding Remoter.finallyArgument
setting. When set to null, the global setting is used.
Default is null (uses global setting from Remoter.finallyArgument
).
It is advisable to set this proptery to false if the callback registered using .finally
is optionally accepting arguments that change the desired behavior of the function when supplied with a value. If that is the case, you might also want to chech if .instanceArgument
should be set to false
as well as it supplies an additional instance argument when your callback function is an anonymous function.
.instanceArgument
.instanceArgument
is a boolean property, that when set to false disables the addition of the instance reference to the arguments list when the .then
, .catch
and .finally
callbacks are invoked for those registered callbacks that do not have a prototype (ArrowFunctions and Bound Functions). When set to true, enables the this behavior. This setting overrides the corresponding Remoter.instanceArgument
setting. When set to null, the global setting is used.
Default is null (uses global setting from Remoter.instanceArgument
).
See chapter on Claim settled Remoter results for a detailed description.
Instantiation shortcuts
To instanciate either a Remoter and it's resolver and rejector or a Promise with a callback, or any combination of those, a remoter instance provides the .remoter
and .promise
properties.
.remoter
The .remoter
property is a circular reference to the remoter instance.
const {remoter, resolve, reject} = new Remoter;
Example:
const Remoter = require('remoter');
function echoFunctionWithOddCallbackAPI(data, onSuccess, onError) {
if (!data)
onError(
new Error(`No data to echo 😢`)
)
else
onSuccess(data);
}
function asyncEchoFunction(data) {
const {remoter, resolve, reject} = new Remoter;
echoFunctionWithOddCallbackAPI(data, resolve, reject);
return remoter;
}
asyncEchoFunction(
'stuff'
).then(
value =>
console.log(value) // Will output: stuff
);
.promise
The .promise
property returns a persistent reference to a native Promise using Promise.resolve(remoter)
that settles with the remoter instance.
const {promise, callback} = new Remoter;
Example:
const fs = require('fs');
const Remoter = require('remoter');
const {promise, callback} = new Remoter;
promise.then(
value =>
console.log(value) // If you happen to have such a file. Will output: stuff
).catch(
error =>
console.error(error) // Otherwise, will output an OS-specific error
);
fs.readFile('message.txt', callback);
The .promise
property creates a promise the first time the property is accessed and returns the same instance for subsequent property access:
const Remoter = require('remoter');
const remoter = new Remoter;
console.log(remoter.promise === remoter.promise); // Will outout: true
Callback generation
For ease of use with callback-based API's a remoter instance offers a callback factory to create plug-in callbacks that can be used to resolve or reject the remoter instance respectively. If the error property in a callback is truthy, the Remoter will reject the Promise with error as value. Otherwise it will resolve the Promise with result as value.
While .callback
, .errorResultCallback
and .resultErrorCallback
always return the same function reference, .customCallback()
creates a new callback every time it is called.
const Remoter = require('remoter');
const remoter = new Remoter;
console.log(remoter.callback === remoter.callback); // Will output: true
console.log(remoter.customCallback(Remoter.CB_RESULT) === remoter.customCallback(Remoter.CB_RESULT)); // Will output: false
.callback
Returns a callback for promisification. See .promise for an example. Alias for .errorResultCallback.
.errorResultCallback
Returns a callback with the signature callback(error, result)
. See .promise for an example. The callback is created the first time the method property is accessed and maintains its reference!
.resultErrorCallback
Returns a callback with the signature callback(result, error)
. The callback is created the first time the method property is accessed and maintains its reference!
.customCallback(argumentToken[, argumentToken2, ..., argumentToken2N])
Generates a new callback function with a custom signature based on the argumentTokens passed.
.callback
and .errorResultCallback
are equivalent to the callback created using .customCallback(Remmoter.CB_ERROR, Remoter.CB_RESULT)
. .resultErrorCallback
is equivalent to the callback created using .customCallback(Remoter.CB_RESULT, Remmoter.CB_ERROR)
.
Each token is only allowed once per callback. The rest tokens CB_ERRORS
and CB_RESULTS
are only allowed at the end of the signature. A signature can only contain either one CB_RESULT
token or one CB_RESULTS
rest token and only either one CB_ERROR
token or one CB_ERRORS
rest token. It is possible to create as many callbacks per remoter instance as needed.
If either a CB_RESULT
token or a CB_RESULTS
rest token and either a CB_RESULT
token or a CB_RESULTS
rest token are present in the same callback signature, the remoter instance is rejected when the error value in the argument at the positition of the error token is truthy, even if the argument at the position of a result token also contains a truthy result value.
const Remoter = require('remoter');
const {promise, callback} = new Remoter;
callback('errors always win! 😈', 'results are ignored 😢');
promise.then(console.log).catch(console.log); // Will output: errors always win! 😈
Argument Tokens
Remoter.CB_ERROR
The argument at the token position will be used as the error value for rejection. Only one CB_ERROR token is allowed and cannot be combined with a CB_ERRORS token.
const EventEmitter = require('events');
const Remoter = require('remoter');
const eventEmitter = new EventEmitter;
const remoter = new Remoter;
const onError = remoter.customCallback(Remoter.CB_ERROR);
eventEmitter.once('error', onError); // is equvalent to: eventEmitter.once('error', remoter.reject);
Remoter.CB_ERRORS
All arguments including and from the token positon will be used to form an array of error values for rejection. This token is only allowd at the very end of the token sequence.
import * as Remoter from 'remoter';
const remoter = new Remoter;
const onError = remoter.customCallback(Remoter.CB_ERRORS);
window.onerror = onError; // On error, will reject with [<errorMsg>, <url>, <lineNumber>, <column>, <errorObj>]
Remoter.CB_RESULT
The argument at the token position will be used as the result value for fulfillment. Only one CB_RESULT token is allowed and cannot be combined with a CB_RESULTS token.
import * as Remoter from 'remoter';
class FakeRequest {
constructor (options = {}) {
this.onError = options.onError;
this.onSuccess = options.onSuccess;
}
load (isSuccess = true) {
if (isSuccess && this.onSuccess)
this.onSuccess('some result')
else if(this.onError)
this.onError('some error');
}
}
const remoter = new Remoter;
const onSuccess = remoter.customCallback(Remoter.CB_RESULT);
const onError = remoter.customCallback(Remoter.CB_ERROR);
const request = new FakeRequest(
{
onError,
onSuccess
}
);
request.load();
remoter.then(console.log); // Will output: some result Remoter [Promise] { 'some result' }
Remoter.CB_RESULTS
All arguments including and from the token positon will be used to form an array of result values for fulfillment. This token is only allowd at the very end of the token sequence.
const Remoter = require('remoter');
const remoter = new Remoter;
const onTimeout = remoter.customCallback(Remoter.CB_RESULTS);
remoter.then(
values =>
console.log( // Will output: All! The! Values!
...values.map(
v =>
v.charAt(0).toUpperCase()+v.slice(1)+'!'
)
)
)
setTimeout(onTimeout, 1e3, 'all', 'the', 'values');
anything else
Any other argument passed to customCallback() will ignore the argument at that position when passed to the generated callback function. It is recommended that you use either null
or undefined
.
const Remoter = require('remoter');
const remoter = new Remoter;
const callback = remoter.customCallback(null, Remoter.CB_ERROR, null, Remoter.CB_RESULTS);
callback(42, false, 'Marvin', 'life', 'the universe', 'and everything');
// 42: will be ignored ❌
// false: is error value but is falsy and will not reject ❌
// 'Marvin': ignores everyone and will be ignored ❌
// 'life', 'the universe', 'and everything': are collapsed into an array that is used to fulfill the promise ✔
remoter.then(values => console.log('The answer to', ...values)); // Will output: The answer to life the universe and everything
Lifecycle Tracing: A glimpse into Schroedinger's Box
Debugging Promises sometimes feels like fiddeling with the cat in the box. To find out if it's dead or alive, you have to open the box. Remoter offers you an event API to trace the lifecycle of a Promise.
Remoter Instance
.on(eventName, callback)
Subscribe a callback to a lifecycle event of a remoter instance. callback will be invoked right before the event occures. The following table lists the events by their eventName and the arguments the callback is invoked with:
|Event Name |Description |Callback Arguments |
|---------------|-------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
|then
|A callback has been registered using .then(callback)
|callback (a reference to the callback that has been registered) |
|catch
|A callback has been registered using .catch(callback)
or .then(..., callback)
|callback (a reference to the callback that has been registered) |
|finally
|A callback has been registered using .finally(callback)
or .then(callback, callback)
|callback (a reference to the callback that has been registered) |
|fulfilled
|The Remoter has been fulfilled with a value or with nothing |value |
|rejected
|The Remoter has been rejected with an error or with nothing |error |
|settled
|The Remoter has been rejected or resolved with a value or error |valueOrerror |
|follows
|The Remoter has been resolved to follow another Promise, Remoter or Thenable |promiseRemoterOrThenable |
|resolved
|The Remoter has been resolved to a value or to follow another Promise, Remoter or Thenable |valuePromiseRemoterOrThenable |
|claimed
|A result has been delivered to a .then
callback |value, callback (a reference to the callback that has been invoked)|
|caught
|An error has been delivered to a .catch
callback |error, callback (a reference to the callback that has been invoked)|
|finalized
|An error or value has been delivered to a .finally
callback |valueOrError, callback (a reference to the callback that has been invoked)|
|oversaturated
|The Remoter was resolved or rejected but has already been settled |valuePromiseRemoterOrThenable |
|*
|Any of the above events is emitted |eventName, ...eventArguments (see above) |
The callback function can be an arrow function, an anonymous function or a named function.
As arrow functions do not have their own this
context an additional argument will be added being a reference to the remoter instance:
const remoter = new Remoter;
remoter.on(
'fulfilled',
(value, remoterInstance) => {
// Will output: Remoter resolved externally with value 42
console.log(`Remoter resolved ${remoterInstance.remote?'externally':'internally'} with value`, value);
}
);
remoter.resolve(42);
Remoter resolved externally with value 42
As named functions and anonymous functions can have their own this
context, this context will be the remoter instance:
const remoter = new Remoter;
remoter.on(
'resolved',
function (value) {
// Will also output: Remoter resolved externally with value 42
console.log(`Remoter resolved ${this.remote?'externally':'internally'} with value`, value);
}
);
remoter.resolve(42);
Remoter resolved externally with value 42
Find multi-settling bugs
If you miss values or receive unexpected values and you suspect that one of your promises gets resolved twice, both rejected and resolved or any wild combination of that, simply make it a Remoter and throw an error within the oversaturated
event.
const yourPromise = new Remoter(/*your executor that causes the bug*/);
yourPromise.on(
'oversaturated',
valueOrError => {
console.log('oversaturation with value:', valueOrError);
throw new Error('Trace the stack trace!');
}
);
The line that caused the 2nd settling invokation will be there in the middle of the stack trace, for sure.
.off([eventName[, callback]])
The .off
method allows you to remove a callback for a given eventName that has previously been attached using the .on method. callback needs to be a reference to the function that has been registered before. If callback is not provided, all callbacks for the given eventName are removed. If called without eventName and callback, .off
will remove all events and remove the introspection tooling from the remoter instance.
Remoter Class
Constructor: new Remoter([executor])
The constructor of Remoter creates a new instance of the Remoter. Unlike the native Promise
- the executor can be omitted and
- the executor does not need to invoke either the
reject
norresolve
callback.
The executor (resolve, reject) => { ... }
does not execute within the native space behind the super()
call as Remoter passes a custom executor to be able to extract the native reject
and resolve
callbacks. To be able to handover the remoter instance as an additional argument to the .then
, .catch
and .finally
callbacks as well as to the lifecycle event listeners, the executor is invoked at the very end of the constructor which means compared to the native Promise there are more calls on the stack until the instance is returned.
Remoter.resolve, Remoter.all, Remoter.any, ...
Again, Remoter is a Promise. The static methods of the native Promise are also available on Remoter. It's totally up to you if you want to use Promise.all([remoter1, promise, remoter2])
or Remoter.all([remoter1, promise, remoter2])
or even Promise.all([remoter1.promise, promise, remoter2.promise])
. Remoter works well with Promise.all
and vice versa. Keep in mind that if you want to use features of Remoter on the Promise returned e.g. by .all
you will have to use Remoter.all
. At the moment the .remote
functionality property and everything related to remote settling introspection is strictly connected to the Promise created by Remoter.all
. It tells you nothing about if any of the remoter instances inside have been resolved from the outside. Additionaly, resolving a remoter instance that has been created using e.g. Remoter.all
from the outside would be a very silly thing to do as it short-circuits the intended functionality. However, you can use this to create a mock for tests:
// Very silly thing to do when not mocking for tests:
const Remoter = require('remoter');
const allRemoters = Remoter.all([new Remoter]);
allRemoters.resolve(['myMockResult']);
For Promise compatibility of Remoter.resolve()
see Chaining.
Lifecycle Introspection Hooks
The Remoter Class singleton can be used as an EventEmitter emiting events when a Remoter instance has been created. This gives you the opportunity to log lifecycle information about every Promise created within your code. However, there is also Async Hooks in Node that give you a much more granular introspection for all asyncronous handles. However, it isn't very easy to understand and not available for web 🤷♀️.
|Event Name |Description |Callback Arguments |
|---------------|----------------------------------------------------------------------------------------|-------------------------------------------------------------------|
|create
|A new Remoter
instance has been created and the Executor will be invoked |remoter (a reference to the remoter instance that has been created)|
|*
|Any of the above events is emitted |eventName, ...eventArguments (see above) |
Remoter.on(eventName, callback)
The Remoter.on
method allows you to attach callbacks that get invoked every time a new Remoter
has been created.
const Remoter = require('remoter');
function logNewRemoter () {
console.log(`Remoter created`, this);
}
Remoter.on('create', logNewRemoter);
const remoter = new Remoter; // Will output: Remoter created Remoter [Promise] <pending>
Remoter.off(eventName[, callback])
The Remoter.off
method allows you to remove a callback that has previously been attached using Remoter.on. callback needs to be a reference to the function that has been registered before. If callback is not provided, all callbacks are removed.
Native Promise Class Property
The Remoter.Promise
property gives read-only access to the native Promise
class.
const Remoter = require('remoter');
console.log(Remoter.Promise === Promise); // Will output: true
This is useful in case you replaced Promise
with Remoter
.
Remoter.finallyArgument
Remoter.finallyArgument
is a boolean property, that when set to false prevents passing of an errorOrValue argument to callbacks registered via .finally
. When set to true, enables the this behavior. This setting can be overridden on the remoter instance with the corresponding .finallyArgument
property. It is advisable to set this proptery to false if the callbacks registered using .finally
are optionally accepting arguments that change the desired behavior of the function when supplied with a value. If that is the case, you might also want to chech if Remoter.instanceArgument
should be set to false
as well as it supplies an additional instance argument when your callback function is an anonymous function.
Default is true.
Remoter.instanceArgument
Remoter.instanceArgument
is a boolean property, that when set to false disables the addition of the instance reference to the arguments list when the .then
, .catch
and .finally
callbacks are invoked for those registered callbacks that do not have a prototype (ArrowFunctions and Bound Functions). When set to true, enables this behavior. This setting can be overridden on the remoter instance with the corresponding .instanceArgument
property. See chapter on Claim settled Remoter results for a detailed description.
Default is true.
Examples
For Sugar
const Remoter = require('remoter');
// Instead of this:
function sleepWithPromise (milliseconds = 0) {
return new Promise(
resolve => setTimeout(resolve, milliseconds)
);
}
// write this:
function sleepWithRemoter (milliseconds = 0) {
const {promise, resolve} = new Remoter;
setTimeout(resolve, milliseconds);
return promise;
}
(async () => {
const sleepTimeMs = 2e3;
console.log('Sleeping for', sleepTimeMs, 'ms 😴 ...');
await sleepWithRemoter(sleepTimeMs);
console.log('Done sleeping 🥱');
})();
Promisify callback-based API's
const fs = require('fs');
const Remoter = require('remoter');
function writeFileAsync (fileName, data, options) {
const { remoter, callback } = new Remoter;
fs.writeFile(fileName, data, options, callback);
return remoter;
}
(async () => {
try {
await writeFileAsync('message.txt', 'stuff');
} catch (error) {
throw error;
}
})()
Avoid async callback hells
// Compare:
function promiseWriteFileAsync (fileName, data, options) {
return new Promise(
(resolve, reject) => {
fs.writeFile(
fileName,
data,
options,
(error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
}
);
}
);
}
// to:
function remoterWriteFileAsync (fileName, data, options) {
const { promise, callback } = new Remoter;
fs.writeFile(fileName, data, options, callback);
return promise;
}
Remotely resolving a Promise
const Remoter = require('remoter');
function resolveIntrinsicly() {
const remoter = new Remoter(
resolve =>
setTimeout(
resolve,
1e3,
'Intrinsically resolved'
)
);
return remoter;
}
function resolveExtrinsicly() {
const remoter = new Remoter;
setTimeout(
remoter.resolve,
1e3,
'Extrinsically resolved'
);
return remoter;
}
async function main() {
await resolveIntrinsicly();
await resolveExtrinsicly();
}
main.bind(this)();
Awaiting Events
const Remoter = require('remoter');
const EventEmitter = require('events');
function nextEventReceived (eventEmitter, eventName) {
const event = new Remoter;
eventEmitter.once(eventName, event.resolve);
return event;
}
async function main () {
const eventEmitter = new EventEmitter();
setTimeout(
(...args) => {
console.log('Emitting event');
eventEmitter.emit(...args);
},
1e3,
'myEvent',
'some payload'
);
console.log('⏳ Waiting for next myEvent');
const payload = await nextEventReceived(eventEmitter, 'myEvent');
console.log('🎉🎉🎉 myEvent received with payload:', payload);
}
main.bind(this)();
const Remoter = require('remoter');
const EventEmitter = require('events');
// Keep in mind that this won't work if the .next frequency (drain) is 'slower'
// than the .emit frequency (event) as events will be lost without a buffer
function* asyncEvents (eventEmitter, eventName) {
while (true) {
const {promise: event, resolve: fired} = new Remoter;
eventEmitter.once(eventName, fired);
yield event;
}
}
// Alternative with Error Event handling
/*
function* asyncEvents (eventEmitter, eventName, errorEventName = 'error') {
while (true) {
const {promise: event, resolve: fired, reject: error} = new Remoter;
eventEmitter.once(errorEventName, error);
eventEmitter.once(
eventName,
(...args) =>
eventEmitter.off(errorEventName, error), // Prevent Event leaking
fired(...args)
);
yield event;
}
}
*/
async function main () {
const eventEmitter = new EventEmitter();
let eventNo = 0;
const interval setInterval(
(...args) => {
console.log('Emitting event');
eventEmitter.emit(...args, eventNo++);
},
1e3,
'myEvent'
);
for await (const payload of asyncEvents(eventEmitter, 'myEvent'))
console.log('myEvent received with payload:', payload);
clearInterval(interval);
}
main.bind(this)();
Cancelling Request Promises
const Remoter = require('remoter');
class FakeRequest {
constructor (url) {
this.running = false;
this.url = url;
this.timeout = null;
}
['get'] (callback) {
this.running = true;
const result = 'some result';
if (this.url)
this.timeout = setTimeout(
callback,
0.5e3,
null,
result
); // Start Fake Request
}
abort () {
clearTimeout(this.timeout); // Cancel Fake Request
this.timeout = null;
this.running = false;
console.log('Aborted', this.constructor.name);
}
}
function request (url) {
const remoter = new Remoter;
const request = new FakeRequest(url);
const timeout = setTimeout(
() => {
console.log('Aborting request because of timeout');
request.abort();
remoter.reject(new Error('Request timed out'));
},
1e3
);
request.get(
(error, result) => {
clearTimeout(timeout);
if (error) {
console.log('Request resulted in an error');
remoter.reject(error);
} else {
console.log('Request successful');
remoter.resolve(result);
}
}
);
console.log('Request started');
return remoter;
}
async function main () {
const result = await request(true);
console.log('1st request resulted in:', result)
try {
await request(undefined);
} catch (error) {
console.log('2nd request resulted in error:', error.message);
}
}
main.bind(this)();
Limiting concurrent requests
const Remoter = require('remoter');
// Let's mock a fetch function for our example:
function fetch (url) {
const {promise, resolve} = new Remoter;
setTimeout(
resolve,
Math.floor(Math.random() * 5e2) + 5e2,
url
);
return promise;
}
class Semaphore {
constructor (max = 1) {
this.flying = [];
this.waiting = [];
this.max = max;
}
_getReleaseToken(token) {
return () => {
this.flying.splice(this.flying.indexOf(token), 1);
this._grantTokens();
}
}
_grantTokens() {
while (this.flying.length < this.max && this.waiting.length > 0) {
const grantedToken = this.waiting.splice(0,1)[0];
const release = this._getReleaseToken(grantedToken);
this.flying.push(grantedToken);
grantedToken.resolve(release);
}
}
getToken(force = false) {
const token = new Remoter;
if (force || this.flying.length < this.max) {
this.flying.push(token);
const release = this._getReleaseToken(token);
token.resolve(release);
} else {
this.waiting.push(token);
}
return token.promise;
}
rejectAll() {
for (const waitingToken of this.waiting)
waitingToken.reject(new Error('wating queue cleared 🤷♂️'));
this.waiting.splice(0, this.waiting.length);
}
}
// Limit concurrent requests to 10 at a time
const semaphore = new Semaphore(10);
async function getData(url) {
console.log('⏳ requesting token for\t\t', url);
const release = await semaphore.getToken();
const result = await fetch(url).finally(release); // Don't forget to release!
console.log('📄 data received for\t\t', url);
return result;
}
function getAllDataFrom(source, numberOfIds) {
const ids = Array(numberOfIds).fill().map((v,i)=>`${source}:${i+1}`);
const data = ids.map(id => getData(id));
return Promise.all(data);
}
async function main () {
const logTick = (start) => {
const timeDiff = ((new Date).getTime() - start.getTime()) / 1000;
console.log(`${timeDiff} I am non-blocking 🤗`);
}
const start = new Date;
const interval = setInterval(
logTick,
1e3,
start
);
logTick(start);
const requestA = getAllDataFrom('A', 100);
const requestB = getAllDataFrom('B', 50);
console.log(
await requestA,
await requestB
);
clearInterval(interval);
}
main.bind(this)();
Crowd wisdom
I want Remoter to be as usefull as possible which includes being as lightweight as possible. I'd love to get your opinion especially on the following topics:
- Should Remoter stay without dependencies? I thought about using EventEmitter and a pro-forma EventTarget as the internal EventEmitter behind the introspection hooks.
- Should the value or error respectively be available as an instance property? And if so, would you prefer the property access to result in an exception when requested before a settling callback was called or would
if (!remoter.settled) console.log(remoter.value);
be fine? What would you use that for? - For the frontend people: Would you like to have more convenience in terms of transpiling/babeling?
- What implementations did you do with Remoter? Which examples are you missing?
ToDo
- Reduce instance heap footprint by moving abstract functions from constructor closure to module scope
- Annotation Slot in the constructor
- Event Timing optimization
- Add more examples
Open to PR's
PR's welcome! Feel free to improve Remoter 🤓. Please find and fix bugs 🙏. Improvement of the docs very welcome!
License
MIT.