Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated csp.CLOSED to Symbol.for('@@csp/CLOSED') with fallback to Object String #61

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

jaawerth
Copy link

Addresses #60 .

Used a String object: Object('csp::CLOSED') rather than an empty object. The result is

  • csp.CLOSED === csp.CLOSED:true
  • csp.CLOSED === 'csp::CLOSED' :false
  • csp.CLOSED == 'csp::CLOSED':true

which I think makes sense for this use-case. This allows null values to be passed into channels without accidentally closing them.

It also (as pointed out to me by @ljharb) allows people to use csp.CLOSED as an object key for mapping errors and the like.

@ljharb
Copy link

ljharb commented May 13, 2015

👍

@jlongster
Copy link
Collaborator

I don't think this does what you think it does. In Firefox:

Object('csp::CLOSED')
String [ "c", "s", "p", ":", ":", "C", "L", "O", "S", "E", 1 more… ]

@ljharb
Copy link

ljharb commented May 13, 2015

@jlongster that's just how the console displays it. Object('foo') is the same as new String('foo').

@jaawerth
Copy link
Author

Yeah, I suppose the one potential issue to this approach vs a plain object is it's potentially confusing in logging situations, but it DOES allow for a bunch of useful applications without interference with the main while (value !== csp.CLOSED) aspect.

For reference,
http://plnkr.co/edit/L0UH02bZcKgA7iBDK4pe?p=preview

CLOSED === "csp::CLOSED"
>> false
CLOSED == "csp::CLOSED"
>> true
CLOSED === CLOSED
>> true
CLOSED === Object("csp::CLOSED")
>> false
var CLOSED = Object("csp::CLOSED");
var o = {}; o[CLOSED] = "hi";
o[CLOSED]
>> hi

@ljharb
Copy link

ljharb commented May 13, 2015

they'll ==, but not ===. However, in a logging situation, it'd likely print out the string in some form.

@anru
Copy link

anru commented May 14, 2015

Much better use Symbol('csp::CLOSED') for csp.CLOSED. Symbols are ideal solution for this case, because Symbols compared by instance and each Symbol is unique:

> Symbol('csp::CLOSED') == Symbol('csp::CLOSED')
false

@anru
Copy link

anru commented May 14, 2015

But Symbols has smaller set of supported browsers, of course

@jlongster
Copy link
Collaborator

yeah, even symbols has problems though since it's too easy to create duplicate packages in npm. What some people are doing are just namespacing strings. That's what we're doing in transducers: cognitect-labs/transducers-js#20

This works when you have multiple versions of the library (the above Object trick wouldn't) and even can be used across multiple libraries (if there is another CSP lib).

So I'd say just do with something like "@@csp/CLOSED", just a raw string. I wish symbols were more prevalent and weren't kind of broken, and this should not be a problem.

I'll really miss being able to do while(yield chan) {}, but I know it's the right move because JavaScript's falsy values are way too loose anyway. It's actually fine to use null for closed in theory (ClojureScript does) but since you'd always need to === null anyway, we should make it something else to force users to do ===.

@jaawerth
Copy link
Author

Incidentally, I'd originally written it with Symbols and a fallback to an empty object, but switched to this after discussing the general practice. Code looked something like this (but changed to using Symbol.for to add it to the runtime Symbol registry, which should solve the multiple implementation issue):

EDIT: Updated to use @@csp/CLOSED instead of csp::CLOSED because.. why not?

var CLOSED;
try {
  CLOSED = Symbol.for('@@csp/CLOSED');
  // check for a properly-behaving ES6 Symbol implementation
  if (typeof CLOSED !== 'symbol') throw new Error('Symbols not supported.');
} catch(e) {
  // could use Object('@@csp/CLOSED') here instead?
  CLOSED = {}; 
}

I went with object strings here on the basis that they accomplish nearly the same effect while being evaluated/compared faster, and wouldn't suffer from the unpredictability of the above (even if that unpredictability SHOULD be moot under most use-cases). The other downside of a Symbol is that if used as a key in a lookup object, it wouldn't appear in Object.keys(obj).

@jlongster good point about other libs and/or other instances, though. However, folks could work around that when necessary by using soft equality. Still, it does feel a little ugly that way - which is a point in favor of @@csp/CLOSED.

@ljharb
Copy link

ljharb commented May 14, 2015

The problem with "sometimes Symbol and sometimes not" is that its use as an object key will differ based on engine support.

What's the benefit of using a Symbol at all, over just a string?

@jaawerth
Copy link
Author

Mainly the idea that a signal token should be unique/difficult to reproduce. It's the inverse of the edge case of interop between instances or implementations. Definitely an edge case, but, for example, someone passes strings through a channel. If they're coming from user input, an unscrupulous user could pass '@@csp/CLOSED' to shut down the pipeline. An implementer would need to either add @@csp/CLOSED to the list of inputs to escape, additional handlers for false positives, or always wrap the strings in an object (granted, it's unlikely they wouldn't be wrapped in the first place).

This thread has brought up some points in favor of using a string that I hadn't thought of, though. Thoughts?

@jlongster
Copy link
Collaborator

The fact is that the csp/ namespacing as well using the traditional @@ syntax to indicate that it represents a symbol pretty much guarantees that a clash will never happen in reality. This is the technique several other libraries use, mainly due to the deficiency of npm and how common it is to have multiple copies (because eventually we could use symbols).

I vote for a normal namespaced string, as it's a lot more predictable.

@ljharb
Copy link

ljharb commented May 14, 2015

What's unpredictable about a String object?

@jlongster
Copy link
Collaborator

What's unpredictable about a String object?

It's printed very strangely in various consoles

@ljharb
Copy link

ljharb commented May 14, 2015

Why does "how a debugger/development console chooses to display it" matter?

@jlongster
Copy link
Collaborator

A developer will see this a lot while debugging? I think we're worrying too much about something that will never happen in production, like I said several other libraries use namespaced strings and it's ok.

EDIT: and it's more predictable regarding multiple versions or other libraries that want to re-use it, since equality will always return true

@jaawerth
Copy link
Author

jaawerth commented Jun 1, 2015

So, having thought about this again, it really comes down to a trade-off: with a pure string approach, a developer would need to escape incoming string data to avoid a minor "Bobby Tables" vulnerability, or - with a non-string approach like the string object - a developer would need a helper function for doing comparisons across platforms or implementations. Something like

var toString = Object.prototype.toString;
function isClosed(token) {
  return token == csp.CLOSED && toString.call(token) === '[object String]';
}

Either way would probably necessitate a helper function to simplify things for a user, the question is which way you want to go. Personally I still think it'd be better to go with the string object approach, or even { CLOSED: true }, but if you do still opt with "@@csp/CLOSED" a provided escape method would definitely be in order.

Still, escaping would also make it impossible to close the channel via input alone, since you wouldn't be able to tell whether the @@csp/CLOSED was coming from manual input or from code.

Granted, I realize the odds are pretty slim for someone to want to cause this by typing in that string. Anyway, if you still want to go the string approach, I'm happy to update this PR accordingly to save you the effort.

@ubolonton
Copy link
Contributor

I would prefer Symbol.for("@@csp/CLOSED"), if there is a good a polyfill implementation (I saw https://www.npmjs.com/package/es6-symbol, but haven't checked it yet). The same go for other special values like NO_VALUE (for poll).

We would need to add proper tests for browsers and multi-version tests for npm though.

@ljharb
Copy link

ljharb commented Aug 9, 2015

Symbol.for, because it is cross-realm, is impossible to faithfully polyfill. Symbols, because of typeof, are impossible to faithfully polyfill.

However, it will work fine if when Symbols aren't available in the environment, it falls back to an Object String.

@igl
Copy link

igl commented Aug 19, 2015

> Object('csp::CLOSED') == 'csp::CLOSED'
true
> Object('csp::CLOSED') === 'csp::CLOSED'
false

+1 for symbols

@jaawerth
Copy link
Author

I think it's generally the consensus that Symbol.for would be the ideal solution - the problem is that they can't be polyfilled in a complete manner, and regular Symbols have the same limitation as Object strings. So if everyone is comfortable with Symbol.for, and a fallback that won't work cross-realm without a more complex checking method, that's fine.

Is that an acceptable level of inconsistency though? It'd look like:

try {
  csp.CLOSED = Symbol.for('@@csp/CLOSED');
  if (typeof csp.CLOSED !== 'symbol') throw new TypeError('Symbols not supported.');
  // probably wouldn't really use arrow fns
  csp.isClosedToken = token => token === csp.CLOSED;
} catch(e) {
  csp.CLOSED = Object('@@csp/CLOSED');
  // this would work cross-realm.
  csp.isClosedToken = token =>
    (token == csp.CLOSED && toString.call(token) === '[object String]');

}

@ljharb
Copy link

ljharb commented Aug 19, 2015

That solution LGTM, especially since you're providing the "is" function.

@jaawerth jaawerth changed the title Updated csp.CLOSED to Object('csp::CLOSED') Updated csp.CLOSED to Symbol.for('@@csp/CLOSED') with fallback to Object String Oct 27, 2015
@jaawerth
Copy link
Author

Merged in the discussed revisions; it'll now default to using a global Symbol via Symbol.for, which will work cross-realm, and defined an isClosedToken function, which will default to straight-up === evaluation, but if typeof CLOSED !== 'symbol', it falls back to value == CLOSED && toString(value) === '[object String]'.

The only downside is the potential complexity of having to use the isClosedToken function when potentially working across realms.

@tejasmanohar
Copy link

Bump. Looks like csp.CLOSED is still null :\

closedToken = Object('@@csp/CLOSED');
}
return closedToken;
})();
Copy link

@btnwtn btnwtn Sep 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense to write this as:

var CLOSED = (function() {
  try {
    return Symbol.for('@@csp/CLOSED');
  } catch(e) {
    return Object('@@csp/CLOSED');
  }
})()

Removes unnecessary variables.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose. Doesn't really matter. Style preference.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works fine, but provides zero value as the engine can optimize those out anyways.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they'll be pretty much equivalent, and I don't mind changing it if people find that version more readable - to me it's six of one, half dozen of the other. Tough to remember a year later, but I likely wrote it with a variable after initially going without the IIFE, and adding it in to avoid a function-wide deopt.

@jaawerth
Copy link
Author

Following up on @tejasmanohar's comment, is this an acceptable approach or is there still some question as to what the preferable approach would be? Would maintainers still prefer to use a different Symbol fallback approach, like a normal namespaced string so === on its own would still work? Or is the delay just a result of the project wide temporary inactivity (I see commits started back up in August)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants