Skip to content
Permalink
Browse files

adding 'eventState(..)' closes #1, improve test suite closes #2

  • Loading branch information...
getify committed Aug 14, 2019
1 parent dee1a1d commit 9490a4356126d2ffc6dbcd55a7673627e7ac8350
Showing with 488 additions and 113 deletions.
  1. +2 −0 .gitignore
  2. +2 −0 .npmignore
  3. +97 −10 README.md
  4. +78 −0 build-core.js
  5. +5 −0 copyright-header.txt
  6. +186 −85 index.js
  7. +17 −0 node-tests.js
  8. +13 −3 package.json
  9. +88 −15 test.js
@@ -0,0 +1,2 @@
node_modules/
dist/
@@ -1,3 +1,5 @@
.npmrc
.npmignore
.gitignore
node_modules/
dist/
107 README.md
@@ -6,13 +6,7 @@

Revocable Queue allows you to read/write a sequence of data values (aka, a queue) asynchronously, similar to streams or observables. But any data/event that is still pending in the queue -- hasn't yet been read -- can be revoked.

To install and use in Node:

```cmd
npm install @getify/revocable-queue
```

**Note:** This library uses ES2018 features so it requires Node 12+.
Some helpers are included to make working with revocable queues easier for some common use-cases, including [`lazyZip(..)`](#lazyzip) and [`eventState(..)`](#eventstate).

## API

@@ -91,7 +85,7 @@ pr.then(function t(get){
});
```

**Note:** The read function (name `get()` here) is only resolved if there's a value ready at the moment. However, it' *possible* (but rare!) that between that moment and when `get()` is called, the value has already been revoked. For this reason, it's recommended to call `get()` as soon as it's received, to significantly reduce the chances of such a race condition. For robustness, always perform the `if` check as illustrated above. If the `get()` call returns `RevocableQueue.EMPTY`, the value was already revoked, and that `get()` function should now be discarded. Call `next()` on the queue again to get a promise for the next `get()` read function.
**Note:** The read function (named `get()` here) is only resolved if there's a value ready at the moment. However, it' *possible* (but rare!) that between that moment and when `get()` is called, the value has already been revoked. For this reason, it's recommended to call `get()` as soon as it's received, to significantly reduce the chances of such a race condition. For robustness, always perform the `if` check as illustrated above. If the `get()` call returns `RevocableQueue.EMPTY`, the value was already revoked, and that `get()` function should now be discarded. Call `next()` on the queue again to get a promise for the next `get()` read function.

### Peeking

@@ -113,11 +107,11 @@ if (
}
```

In this above snippet, the not-so-obvious race condition is that `get1()` may have been resolved signficantly before (or after) `get2()`, so by that time, either value may have been revoked. The `if` statment peeks through each queue's ready-to-read accessor function to ensure the value is indeed *still* ready.
In this above snippet, the not-so-obvious race condition is that `get1` may have been resolved signficantly before (or after) `get2`, so by that time, either underlying value may have been revoked. The `if` statment peeks through each queue's ready-to-read accessor function to ensure the value is indeed *still* ready.

**Note:** There is no race condition between the `get1(false)` and the `get1()` call (or the `get2(..)` calls), because JavaScript is single-threaded. So as long as this code pattern is followed, where the peeking and the reading happen synchronously (no promise/`await` deferral in between!), it's perfectly safe to assume that the peeked value is still ready to read in the next statement. Even if some other code was trying to revoke that value at that exact moment, it would be waiting for this code to finish, and since it's fully read/taken, the revoking would fail.

This synchronizing of lazy asynchronous reads from multiple queues is an expected common use-case for **RevocableQueue**. As such, the [`lazyZip(..)`](#lazy-zip) helper utility is also provided.
This synchronizing of lazy asynchronous reads from multiple queues is an expected common use-case for **RevocableQueue**. As such, the [`lazyZip(..)`](#lazyzip) helper utility is also provided.

### Example

@@ -232,6 +226,99 @@ Here's how to consume that queue using `lazyZip(..)`:

That approach is probably much cleaner in most cases!

### `eventState(..)`

Another use-case for revocable queues and `lazyZip(..)` is listening for alternating events to fire that represent a toggling of a state (between `true` and `false`). The concern is not receiving specific values from these events (as illustrated previously with `lazyZip(..)`), but rather just listening for a signal that all of the activation events for a set of two or more listeners has fired, and that no corresponding deactivation events occured while waiting.

For example: managing a series of network socket connections which fire `"connected"` and `"disconnected"` events, and synchronizing operations to occur only when all the connections are active/connected at the same time.

For this kind of event/state synchronization use case, `eventState(..)` is provided, which wraps `lazyZip(..)` and subscribes to events on `EventEmitter` compatible objects (ie, `.on(..)` for subscribing and `.off(..)` for unsubscribing).

To use `eventState(..)`, pass it an array of two or more objects. Each object should have at a minimum a `listener` property with the `EventEmitter` instance, as well as an `onEvent` property with the name of the activation event to listen for.

Optionally, each of these objects can include an `offEvent` property to name a deactivation event to listen for, and a `status` property (boolean, default: `false`) to initialize the status for each listener:

```js
async function greetings(conn1,conn2,conn3) {
await RevocableQueue.eventState([
{
listener: conn1,
onEvent: "connected",
offEvent: "disconnected",
status: conn1.isConnected
},
{
listener: conn2,
onEvent: "connected",
offEvent: "disconnected",
status: conn2.isConnected
},
{
listener: conn3,
onEvent: "connected",
offEvent: "disconnected",
status: conn3.isConnected
}
]);
broadcastMessage( [conn1,conn2,conn3], "greetings!" );
}
```

This code asserts that the three network socket connection objects (`conn1`, `conn2`, and `conn3`) all emit `"connected"` and `"disconnected"` events, as well as have an `isConnected` boolean property that's `true` when connected or `false` when not. The moment all 3 connections are established simultaneously, the `await` expression will complete and then the `broadcastMessage(..)` operation will be performed.

## Builds

[![npm Module](https://badge.fury.io/js/%40getify%2Frevocable-queue.svg)](https://www.npmjs.org/package/@getify/revocable-queue)

The distribution library file (`dist/rq.js`) comes pre-built with the npm package distribution, so you shouldn't need to rebuild it under normal circumstances.

However, if you download this repository via Git:

1. The included build utility (`build-core.js`) builds (and minifies) `dist/rq.js` from source. **The build utility expects Node.js version 6+.**

2. To install the build and test dependencies, run `npm install` from the project root directory.

- **Note:** This `npm install` has the effect of running the build for you, so no further action should be needed on your part.

3. To manually run the build utility with npm:

```
npm run build
```

4. To run the build utility directly without npm:

```
node build-core.js
```

## Tests

A test suite is included in this repository, as well as the npm package distribution. The default test behavior runs the test suite using `index.js`.

1. The included Node.js test utility (`node-tests.js`) runs the test suite. **This test utility expects Node.js version 6+.**

2. To run the test utility with npm:

```
npm test
```

Other npm test scripts:

* `npm run test:dist` will run the test suite against `dist/rq.js` instead of the default of `index.js`.

* `npm run test:package` will run the test suite as if the package had just been installed via npm. This ensures `package.json`:`main` properly references `dist/rq.js` for inclusion.

* `npm run test:all` will run all three modes of the test suite.

3. To run the test utility directly without npm:

```
node node-tests.js
```

## License

All code and documentation are (c) 2019 Kyle Simpson and released under the [MIT License](http://getify.mit-license.org/). A copy of the MIT License [is also included](LICENSE.txt).
@@ -0,0 +1,78 @@
#!/usr/bin/env node

var fs = require("fs"),
path = require("path"),
ugly = require("terser"),
packageJSON,
copyrightHeader,
version,
year = (new Date()).getFullYear(),

ROOT_DIR = __dirname,
SRC_DIR = ROOT_DIR,
DIST_DIR = path.join(ROOT_DIR,"dist"),

CORE_SRC = path.join(SRC_DIR,"index.js"),
CORE_DIST = path.join(DIST_DIR,"rq.js"),

result = ""
;

console.log("*** Building Core ***");
console.log(`Building: ${CORE_DIST}`);

try {
// try to make the dist directory, if needed
try {
fs.mkdirSync(DIST_DIR,0o755);
}
catch (err) { }

result += fs.readFileSync(CORE_SRC,{ encoding: "utf8", });

result = ugly.minify(result,{
mangle: {
keep_fnames: true,
},
compress: {
keep_fnames: true,
},
output: {
comments: /^!/,
},
});

// was compression successful?
if (!(result && result.code)) {
if (result.error) throw result.error;
else throw result;
}

// read version number from package.json
packageJSON = JSON.parse(
fs.readFileSync(
path.join(ROOT_DIR,"package.json"),
{ encoding: "utf8", },
)
);
version = packageJSON.version;

// read copyright-header text, render with version and year
copyrightHeader = fs.readFileSync(
path.join(SRC_DIR,"copyright-header.txt"),
{ encoding: "utf8", },
).replace(/`/g,"");
copyrightHeader = Function("version","year",`return \`${copyrightHeader}\`;`)( version, year );

// append copyright-header text
result = `${copyrightHeader}${result.code}`;

// write dist
fs.writeFileSync( CORE_DIST, result, { encoding: "utf8", } );

console.log("Complete.");
}
catch (err) {
console.error(err);
process.exit(1);
}
@@ -0,0 +1,5 @@
/*! revocable-queue.js
v${version} (c) ${year} Kyle Simpson
MIT License: http://getify.mit-license.org
*/

0 comments on commit 9490a43

Please sign in to comment.
You can’t perform that action at this time.