Initial release.
This commit is contained in:
commit
8c599e8e5c
13 changed files with 3651 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
coverage
|
||||
node_modules
|
||||
npm-debug.log
|
202
LICENSE
Normal file
202
LICENSE
Normal file
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
230
README.md
Normal file
230
README.md
Normal file
|
@ -0,0 +1,230 @@
|
|||
# Introduction
|
||||
|
||||
This is an idiomatic, minimally-opinionated `Channel` type for JavaScript that's inspired by [Go](https://golang.org/)'s concurrency model. It works in browsers and in Node.js. If you know how to use `Array` then you already know most of how to use `Channel`.
|
||||
|
||||
## Why
|
||||
|
||||
Go's concurrency model is amazing and with JavaScript's async/await feature we have the basis for it as well. All that's missing is a `Channel` type. There are existing libraries that implement one but they're generally ports from other languages and don't feel like they were made for JavaScript. I wanted an idiomatic `Channel` type that's simple and minimally-opinionated.
|
||||
|
||||
This document assumes you're familiar with Go's concurrency model and why you'd want to use it. For explanatory background, read my [blog article](https://www.NodeGuy.com/concurrency-in-javascript) on the subject.
|
||||
|
||||
## Requirements
|
||||
|
||||
ES 2017
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
$ npm install @nodeguy/channel
|
||||
```
|
||||
|
||||
## Basic Use
|
||||
|
||||
Create a channel with `Channel()`.
|
||||
|
||||
To send an value to a channel, use `push`. To receive an value from a channel, use `shift`. Always precede the method calls with `await`:
|
||||
|
||||
```JavaScript
|
||||
const assert = require('assert')
|
||||
const Channel = require('@nodeguy/channel')
|
||||
|
||||
const channel = Channel()
|
||||
|
||||
;(async () => {
|
||||
await channel.push(42)
|
||||
})()
|
||||
|
||||
;(async () => {
|
||||
assert.equal(await channel.shift(), 42)
|
||||
})()
|
||||
```
|
||||
|
||||
The `push` and `shift` methods are usually called in different async functions. They represent the two different ends of the channel and act to coordinate the behavior of the async functions.
|
||||
|
||||
# API
|
||||
|
||||
## New Properties
|
||||
|
||||
The following properties don't have equivalents in `Array`.
|
||||
|
||||
### close() -> async
|
||||
|
||||
Closes the channel so that no more values can be pushed to it. Returns a promise that resolves when any remaining pushes in flight complete.
|
||||
|
||||
Attempting to push to a closed channel will throw an exception and shifting from a closed channel will immediately return `undefined`.
|
||||
|
||||
### readOnly() -> Channel
|
||||
|
||||
Returns a version of the channel that provides only read methods.
|
||||
|
||||
### value
|
||||
|
||||
Set to the most recently `shift`ed value. This is useful when used in combination with `select`.
|
||||
|
||||
### writeOnly() -> Channel
|
||||
|
||||
Returns a version of the channel that provides only write methods.
|
||||
|
||||
### Channel.select(methods) -> async channel
|
||||
|
||||
`Channel.select` attempts to call multiple channel `methods` in parallel and returns the channel of the first one that succeeds. Only the winning method is executed to completion—the other methods have no effect.
|
||||
|
||||
#### Examples
|
||||
|
||||
Imagine you're at a party and your next conversation depends on whom you run
|
||||
into first: Alice, Bob, or Charlie.
|
||||
|
||||
```JavaScript
|
||||
switch (await Channel.select(alice.shift(), bob.shift(), charlie.push(`Hi!`)) {
|
||||
case alice:
|
||||
console.log(`Alice said ${alice.value}.`)
|
||||
break
|
||||
|
||||
case bob:
|
||||
console.log(`Bob said ${bob.value}.`)
|
||||
break
|
||||
|
||||
case charlie:
|
||||
console.log(`I said "hi" to Charlie.`)
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
Be careful of unintended side effects, however. Even though only one value is pushed in the following example, the counter is incremented twice.
|
||||
|
||||
```
|
||||
let counter = 0
|
||||
|
||||
const increment = () => {
|
||||
counter++
|
||||
return counter
|
||||
}
|
||||
|
||||
await Channel.select(alice.push(increment()), bob.push(increment()))
|
||||
assert.equal(counter, 2)
|
||||
```
|
||||
|
||||
Sometimes you don't want to wait until a method completes. You can use this pattern to return immediately even if no methods are ready:
|
||||
|
||||
```JavaScript
|
||||
const closed = Channel()
|
||||
closed.close()
|
||||
|
||||
switch (await Channel.select(alice.shift(), bob.shift(), closed.shift()) {
|
||||
case alice:
|
||||
console.log(`Alice said ${alice.value}.`)
|
||||
break
|
||||
|
||||
case bob:
|
||||
console.log(`Bob said ${bob.value}.`)
|
||||
break
|
||||
|
||||
default:
|
||||
console.log(`No one has anything to say yet.`)
|
||||
}
|
||||
```
|
||||
|
||||
You can also arrange it so that the select completes within a timeout:
|
||||
|
||||
```JavaScript
|
||||
const timeout = Channel()
|
||||
setTimeout(timeout.close, 1000)
|
||||
|
||||
switch (await Channel.select(alice.shift(), bob.shift(), timeout.shift()) {
|
||||
case alice:
|
||||
console.log(`Alice said ${alice.value}.`)
|
||||
break
|
||||
|
||||
case bob:
|
||||
console.log(`Bob said ${bob.value}.`)
|
||||
break
|
||||
|
||||
default:
|
||||
console.log(`I stopped listening after one second.`)
|
||||
}
|
||||
```
|
||||
|
||||
## Array-like Methods
|
||||
|
||||
These methods are similar to the equivalently named methods of `Array`.
|
||||
|
||||
### Channel
|
||||
|
||||
#### Channel([bufferLength = 0]) -> Channel
|
||||
|
||||
Create a new `Channel` with an optional buffer.
|
||||
|
||||
#### Channel.of(...values) -> Channel
|
||||
|
||||
#### Channel.from(iterable | stream.Readable) -> Channel
|
||||
|
||||
Create a new `Channel` from an iterable or a [Node.js readable stream](https://nodejs.org/api/stream.html#stream_readable_streams).
|
||||
|
||||
### Channel Object
|
||||
|
||||
#### filter(callbackfn[, thisArg]) -> Channel
|
||||
#### forEach(callbackfn[, thisArg]) -> async
|
||||
|
||||
The promise returned by `forEach` resolves when the channel is closed:
|
||||
|
||||
```JavaScript
|
||||
const toArray = async (channel) => {
|
||||
const array = []
|
||||
|
||||
await channel.forEach((value) => {
|
||||
array.push(value)
|
||||
})
|
||||
|
||||
return array
|
||||
}
|
||||
```
|
||||
|
||||
If `callbackfn` is async then `forEach` will wait for it before iterating to the next value:
|
||||
|
||||
```JavaScript
|
||||
const pipe = async (source, sink) => {
|
||||
await source.forEach(sink.push)
|
||||
sink.close()
|
||||
}
|
||||
```
|
||||
|
||||
#### join(separator) -> async String
|
||||
#### map(callbackfn[, thisArg]) -> Channel
|
||||
#### push(value) -> async bufferLength
|
||||
|
||||
Unlike `Array`'s method, `push` accepts only one `value` at a time.
|
||||
|
||||
Sends the value into the channel and returns a promise that resolves when the value has been shifted out or placed in the buffer.
|
||||
|
||||
* Throws a `TypeError` when attempting to push to a closed channel.
|
||||
* Throws a `TypeError` when attempting to push `undefined` because it's a reserved value used to indicate a closed channel.
|
||||
|
||||
#### reduce(callbackfn[, initialValue])
|
||||
#### shift() -> Promise
|
||||
|
||||
Returns a promise that resolves when an value is received from the channel. Closed channels always return `undefined` immediately.
|
||||
|
||||
#### slice(start, end) -> Channel
|
||||
|
||||
# Contributing
|
||||
|
||||
Please [submit an issue](https://github.com/NodeGuy/channel/issues/new) if you would like me to make `Channel` more `Array`-like (e.g., by adding another `Array` method).
|
||||
|
||||
# Similar Projects
|
||||
|
||||
* [Channel](https://github.com/gozala/channel)
|
||||
* [cochan](https://github.com/skozin/cochan)
|
||||
* [js-csp](https://github.com/ubolonton/js-csp)
|
||||
* [node-csp](https://github.com/olahol/node-csp)
|
||||
|
||||
# Copyright
|
||||
|
||||
Copyright 2017 [David Braun](https://www.NodeGuy.com/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
these files except in compliance with the License. You may obtain a copy of the
|
||||
License at `http://www.apache.org/licenses/LICENSE-2.0`. Unless required by
|
||||
applicable law or agreed to in writing, software distributed under the License
|
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the specific language
|
||||
governing permissions and limitations under the License.
|
312
lib/index.js
Normal file
312
lib/index.js
Normal file
|
@ -0,0 +1,312 @@
|
|||
'use strict'
|
||||
|
||||
// An order represents a pending push or shift.
|
||||
const Order = (channel) => {
|
||||
let order
|
||||
const preonFulfilleds = []
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
order = {
|
||||
resolve: (value) => {
|
||||
preonFulfilleds.forEach((preonFulfilled) => {
|
||||
preonFulfilled(value)
|
||||
})
|
||||
|
||||
resolve(value)
|
||||
},
|
||||
|
||||
reject
|
||||
}
|
||||
})
|
||||
|
||||
Object.assign(promise, {
|
||||
cancel: () => {
|
||||
order.cancelled = true
|
||||
},
|
||||
|
||||
channel,
|
||||
|
||||
prethen: (onFulfilled) => {
|
||||
preonFulfilleds.push(onFulfilled)
|
||||
}
|
||||
})
|
||||
|
||||
return {order, promise}
|
||||
}
|
||||
|
||||
const Channel = function (bufferLength = 0) {
|
||||
let buffered = 0
|
||||
let closed = false
|
||||
let lastValue
|
||||
let resolvedIndex = 0
|
||||
const pushes = []
|
||||
const shifts = []
|
||||
|
||||
// Process the push and shift queues like an order book, looking for matches.
|
||||
const processOrders = () => {
|
||||
const index = {push: 0, shift: 0}
|
||||
|
||||
// Match pushes and shifts.
|
||||
while ((index.push < pushes.length) && (index.shift < shifts.length)) {
|
||||
const push = pushes[index.push]
|
||||
const shift = shifts[index.shift]
|
||||
|
||||
if (push.cancelled) {
|
||||
index.push++
|
||||
} else if (shift.cancelled) {
|
||||
index.shift++
|
||||
} else {
|
||||
lastValue = push.value
|
||||
shift.resolve(lastValue)
|
||||
buffered = Math.max(0, buffered - 1)
|
||||
index.push++
|
||||
index.shift++
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve push promises up to the end of the buffer.
|
||||
for (
|
||||
;
|
||||
(resolvedIndex < index.push) ||
|
||||
((resolvedIndex < pushes.length) && (buffered < bufferLength));
|
||||
resolvedIndex++
|
||||
) {
|
||||
const {cancelled, resolve} = pushes[resolvedIndex]
|
||||
|
||||
if (!cancelled) {
|
||||
if (resolvedIndex > index.push) {
|
||||
buffered++
|
||||
}
|
||||
|
||||
resolve(bufferLength)
|
||||
}
|
||||
}
|
||||
|
||||
// If the channel is closed then resolve 'undefined' to remaining shifts.
|
||||
if (closed) {
|
||||
for (;
|
||||
index.shift < shifts.length;
|
||||
index.shift++
|
||||
) {
|
||||
const {cancelled, resolve} = shifts[index.shift]
|
||||
|
||||
if (!cancelled) {
|
||||
lastValue = undefined
|
||||
resolve(lastValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pushes.splice(0, index.push)
|
||||
shifts.splice(0, index.shift)
|
||||
resolvedIndex -= index.push
|
||||
}
|
||||
|
||||
const readOnly = Object.freeze({
|
||||
filter: (callbackfn, thisArg) => {
|
||||
const output = Channel()
|
||||
|
||||
;(async () => {
|
||||
for (;;) {
|
||||
const value = await readOnly.shift()
|
||||
|
||||
if (value === undefined) {
|
||||
await output.close()
|
||||
break
|
||||
} else if (callbackfn.call(thisArg, value)) {
|
||||
await output.push(value)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
return output
|
||||
},
|
||||
|
||||
forEach: async (callbackfn, thisArg) => {
|
||||
for (;;) {
|
||||
const value = await readOnly.shift()
|
||||
|
||||
if (value === undefined) {
|
||||
break
|
||||
} else {
|
||||
await callbackfn.call(thisArg, value)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
join: async (separator) => {
|
||||
const elements = []
|
||||
|
||||
await readOnly.forEach((element) => {
|
||||
elements.push(element)
|
||||
})
|
||||
|
||||
return elements.join(separator)
|
||||
},
|
||||
|
||||
map: (callbackfn, thisArg) => {
|
||||
const output = Channel()
|
||||
|
||||
;(async () => {
|
||||
await readOnly.forEach((value) =>
|
||||
output.push(callbackfn.call(thisArg, value))
|
||||
)
|
||||
|
||||
output.close()
|
||||
})()
|
||||
|
||||
return output
|
||||
},
|
||||
|
||||
readOnly: () => readOnly,
|
||||
|
||||
reduce: async (callbackfn, ...initialValue) => {
|
||||
let previousValue = initialValue[0]
|
||||
let previousValueDefined = initialValue.length > 0
|
||||
|
||||
await readOnly.forEach((currentValue) => {
|
||||
if (previousValueDefined) {
|
||||
previousValue = callbackfn(previousValue, currentValue, 0, readOnly)
|
||||
} else {
|
||||
previousValue = currentValue
|
||||
previousValueDefined = true
|
||||
}
|
||||
})
|
||||
|
||||
return previousValue
|
||||
},
|
||||
|
||||
shift: function () {
|
||||
const {order, promise} = Order(this)
|
||||
shifts.push(order)
|
||||
setImmediate(processOrders)
|
||||
return Object.freeze(promise)
|
||||
},
|
||||
|
||||
slice: (start, end = Infinity) => {
|
||||
const output = Channel()
|
||||
|
||||
;(async () => {
|
||||
for (let index = 0; index < end; index++) {
|
||||
const value = await readOnly.shift()
|
||||
|
||||
if (value === undefined) {
|
||||
break
|
||||
} else if (index >= start) {
|
||||
await output.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
await output.close()
|
||||
})()
|
||||
|
||||
return output
|
||||
},
|
||||
|
||||
get value () {
|
||||
return lastValue
|
||||
}
|
||||
})
|
||||
|
||||
const writeOnly = Object.freeze({
|
||||
close: () =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (closed) {
|
||||
reject(new Error(`Can't close an already-closed channel.`))
|
||||
} else {
|
||||
closed = true
|
||||
processOrders()
|
||||
|
||||
// Give remaining orders in flight time to resolve before returning.
|
||||
setImmediate(resolve)
|
||||
}
|
||||
}),
|
||||
|
||||
push: function (value) {
|
||||
const {order, promise} = Order(this)
|
||||
order.value = value
|
||||
|
||||
if (closed) {
|
||||
order.reject(new Error(`Can't push to closed channel.`))
|
||||
} else if (value === undefined) {
|
||||
order.reject(new TypeError(
|
||||
`Can't push 'undefined' to channel, use close instead.`
|
||||
))
|
||||
} else {
|
||||
pushes.push(order)
|
||||
setImmediate(processOrders)
|
||||
}
|
||||
|
||||
return Object.freeze(promise)
|
||||
},
|
||||
|
||||
writeOnly: () => writeOnly
|
||||
})
|
||||
|
||||
// Use Object.create because readOnly has a getter.
|
||||
return Object.freeze(Object.assign(Object.create(readOnly), writeOnly))
|
||||
}
|
||||
|
||||
Channel.from = (values) => {
|
||||
const channel = Channel()
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
// iterable
|
||||
for (let value of values) {
|
||||
await channel.push(value)
|
||||
}
|
||||
|
||||
await channel.close()
|
||||
} catch (exception) {
|
||||
// Assume it's a Node.js stream.readable.
|
||||
|
||||
values.on('readable', async () => {
|
||||
while (true) {
|
||||
const data = values.read()
|
||||
|
||||
if (data === null) {
|
||||
break
|
||||
} else {
|
||||
await channel.push(data)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
values.once('end', channel.close)
|
||||
}
|
||||
})()
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
Channel.of = (...values) =>
|
||||
Channel.from(values)
|
||||
|
||||
Channel.select = (...methodPromises) => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
methodPromises.forEach(async (promise) => {
|
||||
promise.prethen(() => {
|
||||
// We've been given a heads-up that this method will complete first so
|
||||
// cancel the other method calls.
|
||||
methodPromises.forEach((other) => {
|
||||
if (other !== promise) {
|
||||
other.cancel()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
await promise
|
||||
} catch (exception) {
|
||||
reject(exception)
|
||||
}
|
||||
|
||||
resolve(promise.channel)
|
||||
})
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
module.exports = Object.freeze(Channel)
|
2267
package-lock.json
generated
Normal file
2267
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
48
package.json
Normal file
48
package.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "@nodeguy/channel",
|
||||
"description": "a Channel type for Go-like concurrency",
|
||||
"version": "0.0.0",
|
||||
"author": {
|
||||
"name": "David Braun",
|
||||
"email": "David@NodeGuy.com",
|
||||
"url": "https://www.NodeGuy.com/"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/NodeGuy/channel/issues"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@nodeguy/assert": "0.1.1",
|
||||
"husky": "0.14.3",
|
||||
"mocha": "3.5.3",
|
||||
"standard": "10.0.3"
|
||||
},
|
||||
"homepage": "https://github.com/NodeGuy/channel",
|
||||
"keywords": [
|
||||
"CSP",
|
||||
"channel",
|
||||
"concurrency"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/NodeGuy/channel.git"
|
||||
},
|
||||
"scripts": {
|
||||
"precommit": "standard",
|
||||
"pretest": "standard",
|
||||
"test": "mocha --recursive"
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"after",
|
||||
"afterEach",
|
||||
"before",
|
||||
"beforeEach",
|
||||
"describe",
|
||||
"it",
|
||||
"run"
|
||||
]
|
||||
}
|
||||
}
|
1
test/go/README.md
Normal file
1
test/go/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
The tests in this directory are translated from [Go's tests](https://github.com/golang/go/tree/bad5abf64d76f9c302c084c5f62e6f70920d3c81/test/chan).
|
77
test/go/doubleselect.js
Normal file
77
test/go/doubleselect.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Test the situation in which two cases of a select can
|
||||
// both end up running. See http://codereview.appspot.com/180068.
|
||||
|
||||
'use strict'
|
||||
|
||||
const Channel = require('../../lib')
|
||||
|
||||
it(`doubleselect`, async function () {
|
||||
this.timeout(10 * 1000)
|
||||
const iterations = 100000 // number of iterations
|
||||
|
||||
// sender sends a counter to one of four different channels. If two cases both
|
||||
// end up running in the same iteration, the same value will be sent to two
|
||||
// different channels.
|
||||
const sender = async (n, c1, c2, c3, c4) => {
|
||||
for (let i = 0; i < n; i++) {
|
||||
await Channel.select(c1.push(i), c2.push(i), c3.push(i), c4.push(i))
|
||||
}
|
||||
|
||||
c1.close()
|
||||
c2.close()
|
||||
c3.close()
|
||||
c4.close()
|
||||
}
|
||||
|
||||
// mux receives the values from sender and forwards them onto another channel.
|
||||
// It would be simpler to just have sender's four cases all be the same
|
||||
// channel, but this doesn't actually trigger the bug.
|
||||
const mux = async (output, input, done) => {
|
||||
await input.forEach(async (value) => {
|
||||
await output.push(value)
|
||||
})
|
||||
|
||||
await done.push(true)
|
||||
}
|
||||
|
||||
// recver gets a steam of values from the four mux's and checks for
|
||||
// duplicates.
|
||||
const recver = (input) => {
|
||||
const seen = new Map()
|
||||
|
||||
input.forEach((v) => {
|
||||
if (seen.has(v)) {
|
||||
throw new Error(`got duplicate value: ${v}`)
|
||||
}
|
||||
|
||||
seen.set(v, true)
|
||||
})
|
||||
}
|
||||
|
||||
const c1 = Channel()
|
||||
const c2 = Channel()
|
||||
const c3 = Channel()
|
||||
const c4 = Channel()
|
||||
const done = Channel()
|
||||
const cmux = Channel()
|
||||
sender(iterations, c1, c2, c3, c4)
|
||||
mux(cmux, c1, done)
|
||||
mux(cmux, c2, done)
|
||||
mux(cmux, c3, done)
|
||||
mux(cmux, c4, done)
|
||||
|
||||
// We keep the recver because it might catch more bugs in the future.
|
||||
// However, the result of the bug linked to at the top is that we'll
|
||||
// end up panicking with: "throw: bad g->status in ready".
|
||||
recver(cmux)
|
||||
|
||||
await done.shift()
|
||||
await done.shift()
|
||||
await done.shift()
|
||||
await done.shift()
|
||||
cmux.close()
|
||||
})
|
61
test/go/fifo.js
Normal file
61
test/go/fifo.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Test that unbuffered channels act as pure fifos.
|
||||
|
||||
'use strict'
|
||||
|
||||
const Channel = require('../../lib')
|
||||
|
||||
it(`fifo`, function () {
|
||||
const N = 10
|
||||
|
||||
const AsynchFifo = async () => {
|
||||
const ch = Channel(10)
|
||||
|
||||
for (let i = 0; i < N; i++) {
|
||||
await ch.push(i)
|
||||
}
|
||||
|
||||
for (let i = 0; i < N; i++) {
|
||||
if (await ch.shift() !== i) {
|
||||
throw new Error(`bad receive`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Chain = async (ch, val, input, output) => {
|
||||
await input.shift()
|
||||
|
||||
if (await ch.shift() !== val) {
|
||||
throw new Error(val)
|
||||
}
|
||||
|
||||
await output.push(1)
|
||||
}
|
||||
|
||||
// thread together a daisy chain to read the elements in sequence
|
||||
const SynchFifo = async () => {
|
||||
const ch = Channel()
|
||||
let input = Channel()
|
||||
let start = input
|
||||
|
||||
for (let i = 0; i < N; i++) {
|
||||
const output = Channel()
|
||||
Chain(ch, i, input, output)
|
||||
input = output
|
||||
}
|
||||
|
||||
await start.push(0)
|
||||
|
||||
for (let i = 0; i < N; i++) {
|
||||
await ch.push(i)
|
||||
}
|
||||
|
||||
await input.shift()
|
||||
}
|
||||
|
||||
AsynchFifo()
|
||||
SynchFifo()
|
||||
})
|
33
test/go/goroutines.js
Normal file
33
test/go/goroutines.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Torture test for goroutines.
|
||||
// Make a lot of goroutines, threaded together, and tear them down cleanly.
|
||||
|
||||
'use strict'
|
||||
|
||||
const Channel = require('../../lib')
|
||||
|
||||
it(`goroutines`, async function () {
|
||||
const f = async (left, right) => {
|
||||
await left.push(await right.shift())
|
||||
}
|
||||
|
||||
const n = 10000
|
||||
const leftmost = Channel()
|
||||
let right = leftmost
|
||||
let left = leftmost
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
right = Channel()
|
||||
f(left, right)
|
||||
left = right
|
||||
}
|
||||
|
||||
;(async (c) => {
|
||||
await c.push(1)
|
||||
})(right)
|
||||
|
||||
await leftmost.shift()
|
||||
})
|
73
test/go/select.js
Normal file
73
test/go/select.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Test simple select.
|
||||
|
||||
'use strict'
|
||||
|
||||
const Channel = require('../../lib')
|
||||
|
||||
it(`select`, async function () {
|
||||
const closed = Channel()
|
||||
closed.close()
|
||||
let counter = 0
|
||||
let shift = 0
|
||||
|
||||
const GetValue = () => {
|
||||
counter++
|
||||
return 1 << shift
|
||||
}
|
||||
|
||||
const Send = async (a, b) => {
|
||||
let done = false
|
||||
let i = 0
|
||||
|
||||
do {
|
||||
switch (await Channel.select(a.push(GetValue()), b.push(GetValue()),
|
||||
closed.shift())) {
|
||||
case a:
|
||||
i++
|
||||
a = Channel()
|
||||
break
|
||||
|
||||
case b:
|
||||
i++
|
||||
b = Channel()
|
||||
break
|
||||
|
||||
default:
|
||||
done = true
|
||||
}
|
||||
|
||||
shift++
|
||||
} while (!done)
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
let a = Channel(1)
|
||||
let b = Channel(1)
|
||||
let v = await Send(a, b)
|
||||
|
||||
if (v !== 2) {
|
||||
throw new Error(`Send returned ${v} !== 2`)
|
||||
}
|
||||
|
||||
const av = await a.shift()
|
||||
const bv = await b.shift()
|
||||
|
||||
if ((av | bv) !== 3) {
|
||||
throw new Error(`bad values ${av} ${bv}`)
|
||||
}
|
||||
|
||||
v = await Send(a, Channel())
|
||||
|
||||
if (v !== 1) {
|
||||
throw new Error(`Send returned ${v} !== 1`)
|
||||
}
|
||||
|
||||
if (counter !== 10) {
|
||||
throw new Error(`counter is ${counter} !== 10`)
|
||||
}
|
||||
})
|
339
test/index.js
Normal file
339
test/index.js
Normal file
|
@ -0,0 +1,339 @@
|
|||
'use strict'
|
||||
|
||||
const assert = require('@nodeguy/assert')
|
||||
const Channel = require('../lib')
|
||||
const stream = require('stream')
|
||||
|
||||
const toArray = async (channel) => {
|
||||
const array = []
|
||||
|
||||
await channel.forEach((item) => {
|
||||
array.push(item)
|
||||
})
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
describe(`Channel`, function () {
|
||||
it(`allows the use of new`, function () {
|
||||
return new Channel()
|
||||
})
|
||||
|
||||
it(`is frozen`, function () {
|
||||
assert.throws(() => {
|
||||
Channel.frozen = false
|
||||
})
|
||||
})
|
||||
|
||||
it(`creates a frozen object`, function () {
|
||||
assert.throws(() => {
|
||||
Channel().frozen = false
|
||||
})
|
||||
})
|
||||
|
||||
it(`creates a buffered channel`, async function () {
|
||||
const channel = Channel(2)
|
||||
|
||||
;(async () => {
|
||||
assert.equal(await channel.shift(), 0)
|
||||
})()
|
||||
|
||||
await channel.push(0, 1, 2)
|
||||
})
|
||||
|
||||
describe(`from`, function () {
|
||||
it(`iterable`, async function () {
|
||||
assert.deepEqual(await toArray(Channel.from([0, 1, 2])), [0, 1, 2])
|
||||
})
|
||||
|
||||
it(`Node.js's stream.readOnly`, async function () {
|
||||
const readOnly = stream.PassThrough({objectMode: true})
|
||||
readOnly.write(0)
|
||||
readOnly.write(1)
|
||||
readOnly.end(2)
|
||||
assert.deepEqual(await toArray(Channel.from(readOnly)), [0, 1, 2])
|
||||
})
|
||||
})
|
||||
|
||||
it(`of`, async function () {
|
||||
assert.deepEqual(await toArray(Channel.of(0, 1, 2)), [0, 1, 2])
|
||||
})
|
||||
|
||||
describe(`select`, function () {
|
||||
it(`miscellaneous`, async function () {
|
||||
const a = Channel()
|
||||
const b = Channel()
|
||||
|
||||
;(async () => {
|
||||
await b.push(0)
|
||||
await a.push(1)
|
||||
await a.shift()
|
||||
})()
|
||||
|
||||
assert.equal(await Channel.select(a.shift(), b.shift()), b)
|
||||
assert.equal(b.value, 0)
|
||||
assert.equal(await a.shift(), 1)
|
||||
assert.equal(await Channel.select(a.push(0), b.shift()), a)
|
||||
})
|
||||
})
|
||||
|
||||
it(`allows for non-blocking selects`, async function () {
|
||||
const a = Channel()
|
||||
const b = Channel()
|
||||
const nonBlocking = Channel()
|
||||
nonBlocking.close()
|
||||
|
||||
switch (await Channel.select(a.shift(), b.push(0), nonBlocking.shift())) {
|
||||
case a:
|
||||
assert(false)
|
||||
break
|
||||
|
||||
case b:
|
||||
assert(false)
|
||||
break
|
||||
|
||||
default:
|
||||
assert(true)
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe(`Channel object`, function () {
|
||||
describe(`close`, function () {
|
||||
it(`can't close an already closed channel`, function (done) {
|
||||
const channel = Channel()
|
||||
channel.close()
|
||||
|
||||
channel.close()
|
||||
.then(() => {
|
||||
done(new Error())
|
||||
})
|
||||
.catch((reason) => {
|
||||
assert.deepEqual(
|
||||
reason,
|
||||
new Error(`Can't close an already-closed channel.`)
|
||||
)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it(`can't push to a closed channel`, async function () {
|
||||
const channel = Channel()
|
||||
channel.close()
|
||||
|
||||
return (async () => {
|
||||
await channel.push(0)
|
||||
})().then(() => {
|
||||
assert(false)
|
||||
}).catch((reason) => {
|
||||
assert.deepEqual(reason, new Error(`Can't push to closed channel.`))
|
||||
})
|
||||
})
|
||||
|
||||
it(`returns 'undefined' immediately from shift`, async function () {
|
||||
const channel = Channel()
|
||||
channel.close()
|
||||
assert.strictEqual(await channel.shift(), undefined)
|
||||
})
|
||||
})
|
||||
|
||||
it(`filter`, async function () {
|
||||
assert.deepEqual(
|
||||
await toArray(Channel.of(0, 1, 2, 3, 4, 5).filter(x => x % 2 !== 0)),
|
||||
[1, 3, 5]
|
||||
)
|
||||
})
|
||||
|
||||
it(`forEach`, async function () {
|
||||
const output = Channel()
|
||||
|
||||
;(async () => {
|
||||
await Channel.of(0, 1, 2).forEach(output.push)
|
||||
output.close()
|
||||
})()
|
||||
|
||||
assert.deepEqual(await toArray(output), [0, 1, 2])
|
||||
})
|
||||
|
||||
it(`join`, async function () {
|
||||
assert.equal(await Channel.of(`a`, `b`, `c`).join(), `a,b,c`)
|
||||
})
|
||||
|
||||
it(`map`, async function () {
|
||||
assert.deepEqual(
|
||||
await toArray(Channel.of(`a`, `b`, `c`).map(x => x.toUpperCase())),
|
||||
[`A`, `B`, `C`]
|
||||
)
|
||||
})
|
||||
|
||||
describe(`push`, function () {
|
||||
it(`with shift`, async function () {
|
||||
const channel = Channel()
|
||||
|
||||
;(async () => {
|
||||
await channel.push(0)
|
||||
})()
|
||||
|
||||
assert.equal(await channel.shift(), 0)
|
||||
})
|
||||
|
||||
describe(`undefined`, function () {
|
||||
it(`outside select`, function () {
|
||||
const channel = Channel()
|
||||
|
||||
return (async () => {
|
||||
await channel.push(undefined)
|
||||
})().then(() => {
|
||||
assert(false)
|
||||
}).catch((reason) => {
|
||||
assert.deepEqual(
|
||||
reason,
|
||||
new TypeError(`Can't push 'undefined' to channel, use close instead.`)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it(`inside select`, function () {
|
||||
const channel = Channel()
|
||||
|
||||
return (async () => {
|
||||
await Channel.select(channel.push(undefined))
|
||||
})().then(() => {
|
||||
assert(false)
|
||||
}).catch((reason) => {
|
||||
assert.deepEqual(
|
||||
reason,
|
||||
new TypeError(`Can't push 'undefined' to channel, use close instead.`)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it(`returns a frozen promise`, function () {
|
||||
assert.throws(() => {
|
||||
Channel().push(0).frozen = false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it(`readOnly`, async function () {
|
||||
const channel = Channel()
|
||||
const readOnly = channel.readOnly()
|
||||
|
||||
assert.throws(() => {
|
||||
readOnly.close()
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
readOnly.push(0)
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
readOnly.writeOnly()
|
||||
})
|
||||
|
||||
;(async () => {
|
||||
await channel.push(1)
|
||||
})()
|
||||
|
||||
assert.equal(readOnly.readOnly(), readOnly)
|
||||
assert.equal(await readOnly.shift(), 1)
|
||||
assert.equal(readOnly.value, 1)
|
||||
|
||||
assert.throws(() => {
|
||||
readOnly.frozen = false
|
||||
})
|
||||
})
|
||||
|
||||
it(`reduce`, async function () {
|
||||
assert.equal(await Channel.of(0, 1, 2)
|
||||
.reduce((previous, current) => previous + current),
|
||||
3
|
||||
)
|
||||
|
||||
assert.equal(await Channel.of(0, 1, 2)
|
||||
.reduce((previous, current) => previous + current, 10),
|
||||
13
|
||||
)
|
||||
})
|
||||
|
||||
describe(`shift`, function () {
|
||||
it(`with push`, async function () {
|
||||
const channel = Channel()
|
||||
|
||||
;(async () => {
|
||||
await channel.push(0)
|
||||
})()
|
||||
|
||||
assert.equal(await channel.shift(), 0)
|
||||
})
|
||||
|
||||
it(`returns a frozen promise`, function () {
|
||||
assert.throws(() => {
|
||||
Channel().shift().frozen = false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(`slice`, function () {
|
||||
it(`start`, async function () {
|
||||
assert.deepEqual(await toArray(Channel.of(0, 1, 2).slice(1)), [1, 2])
|
||||
})
|
||||
|
||||
it(`end`, async function () {
|
||||
assert.deepEqual(
|
||||
await toArray(Channel.of(0, 1, 2, 3, 4).slice(1, 4)),
|
||||
[1, 2, 3]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it(`value`, async function () {
|
||||
const channel = Channel()
|
||||
|
||||
;(async () => {
|
||||
await channel.push(0)
|
||||
})()
|
||||
|
||||
await channel.shift()
|
||||
assert.equal(channel.value, 0)
|
||||
|
||||
assert.throws(() => {
|
||||
channel.value = 1
|
||||
})
|
||||
|
||||
channel.close()
|
||||
await channel.shift()
|
||||
assert.equal(channel.value, undefined)
|
||||
})
|
||||
|
||||
describe(`writeOnly`, function () {
|
||||
it(`provides only write methods`, async function () {
|
||||
const channel = Channel()
|
||||
const writeOnly = channel.writeOnly()
|
||||
|
||||
assert.throws(() => {
|
||||
writeOnly.readOnly()
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
writeOnly.shift()
|
||||
})
|
||||
|
||||
assert.equal(writeOnly.value, undefined)
|
||||
|
||||
;(async () => {
|
||||
await channel.shift()
|
||||
})()
|
||||
|
||||
await writeOnly.push(0)
|
||||
writeOnly.close()
|
||||
|
||||
assert.throws(() => {
|
||||
writeOnly.frozen = false
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
5
wallaby.js
Normal file
5
wallaby.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = () => ({
|
||||
files: ['lib/**/*.js'],
|
||||
tests: ['test/*.js'],
|
||||
env: {type: 'node'}
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue