web-streams-polyfill icon indicating copy to clipboard operation
web-streams-polyfill copied to clipboard

Enforce privacy for stream internals?

Open wabain opened this issue 4 years ago • 4 comments

At the moment the polyfill follows the reference implementation in keeping private instance data in fields with leading underscores:

export class ReadableStream<R = any> {
  /** @internal */
  _state!: ReadableStreamState;
  /** @internal */
  _reader: ReadableStreamReader<R> | undefined;
  /** @internal */
  _storedError: any;
  /** @internal */
  _disturbed!: boolean;
  /** @internal */
  _readableStreamController!: ReadableStreamDefaultController<R> | ReadableByteStreamController;

  // ...
}

For some use cases it would be nice to have the internals hidden in a more enforced way. An obvious approach would be to use ES private fields, but polyfilling them accurately requires WeakMaps, so it would be necessary to have some kind of additional processing using something like the babel loose transform mode to support pure ES5 targets (without actually enforcing privacy for them, which seems reasonable). I don't think TypeScript offers this itself at the moment.

Would you be interested in accepting a patch along those lines? Obviously adopting this for the whole polyfill would be a bit of an invasive change.

wabain avatar Feb 23 '21 16:02 wabain

I understand where you're coming from, I would also prefer the polyfill to keep its internals a bit more private. However, I am worried about the downleveling to ES5. Even with Babel's loose mode, each property access or method call becomes an ugly call through a Babel helper function... 😞

An alternative solution would be to use a "semi-private" Symbol to hold the actual implementation, and have the public properties and methods go through this symbol. Something like:

class ReadableStreamImpl {
  _state: ReadableStreamState;
  // ...
}

const impl = Symbol('impl');
class ReadableStream {
  readonly [impl]: ReadableStreamImpl;
  constructor() {
    this[impl] = new ReadableStreamImpl();
  }
  
  getReader() {
    return this[impl].getReader();
  }
}

You could still find the private implementation through Object.getOwnPropertySymbols(), so it wouldn't be truly private. For ES5, we could turn Symbol('impl') into impl or _impl or whatever, so it'd still be a valid property key.

This is kind of how the reference implementation works today, but they use webidl2js to generate the boilerplate code in ReadableStream. I don't know if I'd want that for the polyfill though... 😬

MattiasBuelens avatar Feb 23 '21 18:02 MattiasBuelens

That looks like a reasonable approach! I'll take a look at what the webidl2js transform does.

wabain avatar Feb 26 '21 19:02 wabain

Just a heads up, looks like it would produce similar problem like the one i just found out about in #75 (about using two different versions) Some stuff can be truly private but not everything

jimmywarting avatar May 30 '21 20:05 jimmywarting

For #75, I would rather restrict the polyfill to only accept streams from its own version, rather than trying to make multiple versions compatible with each other. For example, readableFromVersion1.pipeTo(writableFromVersion2) should always immediately reject with a TypeError here, indicating that the given writable is not valid.

This should still work even if we decide to use private symbols or private fields. But I'll keep in mind to add a test for that. 😁

MattiasBuelens avatar May 30 '21 20:05 MattiasBuelens