refactor(stream): Give types unto TypeScript
MODIFY
package.json
package.json
"packages/static-module-record",
"packages/stream",
"packages/stream-json",
+ "packages/stream-types-test",
"packages/stream-node",
"packages/syrup",
"packages/test262-runner",

ADD
packages/stream-types-test/package.json
+{
+ "name": "@endo/stream-types-test",
+ "version": "0.1.10",
+ "private": true,
+ "description": "TypeScript validation for Endo stream types.",
+ "keywords": [],
+ "author": "Endo contributors",
+ "license": "Apache-2.0",
+ "homepage": "https://github.com/endojs/endo/tree/master/packages/stream-types-test#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/endojs/endo.git"
+ },
+ "bugs": {
+ "url": "https://github.com/endojs/endo/issues"
+ },
+ "type": "module",
+ "exports": {},
+ "scripts": {
+ "build": "exit 0",
+ "lint": "yarn lint:types && yarn lint:ts",
+ "lint-fix": "eslint --fix . --ext .ts",
+ "lint:ts": "eslint . --ext .ts",
+ "lint:types": "tsc --build tsconfig.json",
+ "test": "yarn lint:types"
+ },
+ "dependencies": {
+ "@endo/stream": "^0.1.0",
+ "ses": "^0.15.3"
+ },
+ "devDependencies": {
+ "@endo/eslint-config": "^0.3.20",
+ "babel-eslint": "^10.0.3",
+ "eslint": "^7.23.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.9.0",
+ "eslint-plugin-eslint-comments": "^3.1.2",
+ "eslint-plugin-import": "^2.19.1",
+ "eslint-plugin-prettier": "^3.1.2",
+ "prettier": "^1.19.1",
+ "typescript": "^4.0.5"
+ },
+ "files": [],
+ "publishConfig": {
+ "access": "private"
+ },
+ "eslintConfig": {
+ "extends": [
+ "@endo"
+ ]
+ },
+ "prettier": {
+ "trailingComma": "all",
+ "singleQuote": true
+ }
+}
+

ADD
packages/stream-types-test/tsconfig.json
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "strict": true,
+ "strictNullChecks": true,
+ "noImplicitAny": true,
+ "downlevelIteration": true,
+ "strictNullChecks": true
+ },
+ "include": ["**/*.ts", "**/*.js"]
+}

ADD
packages/stream-types-test/validation.ts
+/* eslint-disable no-unused-vars, no-underscore-dangle, no-empty */
+/// <reference types="ses"/>
+
+import { makeQueue, makeStream, makePipe } from '@endo/stream';
+// eslint-disable-next-line
+import type { Stream, Reader, Writer } from '@endo/stream';
+
+async () => {
+ const q = makeQueue<number>();
+ q.put(1);
+ q.put(Promise.resolve(1));
+ // @ts-expect-error
+ q.put('NaN'); // it's a string!
+ const a : number = await q.get();
+}
+
+async () => {
+ const q = makeQueue<IteratorResult<number>>();
+ const r = makeQueue<IteratorResult<undefined>>();
+ const s = makeStream(q, r);
+ for await (const value of s) {
+ const cast : number = value;
+ }
+
+ const t = makeStream(r, q);
+ // @ts-expect-error
+ for await (const value of t) {}
+};
+
+async () => {
+ const [reader, writer] = makePipe<number>();
+ for await (const _ of reader) {}
+ await writer.next(1);
+ // @ts-expect-error
+ await writer.next('NaN');
+ // @ts-expect-error
+ for await (const _ of writer) {}
+};
+
+async () => {
+ const [reader, writer] = makePipe<undefined, number>();
+ const r : IteratorResult<undefined> = await reader.next(1);
+ // @ts-expect-error
+ const s : IteratorResult<undefined> = await reader.next('NaN');
+ // @ts-expect-error
+ for await (const _ of reader) {}
+ for await (const value of writer) {
+ const cast : number = value;
+ }
+};
+
+async () => {
+ const [reader, writer] = makePipe<number, string, boolean, Array<number>>();
+ const a: IteratorResult<number, boolean> = await reader.next('A');
+ const b: IteratorResult<number, boolean> = await reader.return([1, 2, 3]);
+ const c: IteratorResult<number, boolean> = await reader.throw(new Error('Abort'));
+ const d: IteratorResult<string, Array<number>> = await writer.next(1);
+ const e: IteratorResult<string, Array<number>> = await writer.return(true);
+ const f: IteratorResult<string, Array<number>> = await writer.throw(new Error('Abort'));
+};

MODIFY
packages/stream/index.d.ts
packages/stream/index.d.ts
-export { makeQueue, makeStream, makePipe } from './index.js';
-export type { Stream, Reader, Writer, AsyncQueue } from './index.js';
+export * from './types.js';
+import { AsyncQueue, Stream, Reader, Writer } from './types.js';
+
+export function makeQueue<TValue>(): AsyncQueue<TValue>;
+
+export function makeStream<
+ TRead,
+ TWrite = unknown,
+ TReadReturn = unknown,
+ TWriteReturn = unknown,
+>(
+ acks: AsyncQueue<IteratorResult<TRead, TReadReturn>>,
+ data: AsyncQueue<IteratorResult<TWrite, TWriteReturn>>,
+): Stream<TRead, TWrite, TReadReturn, TWriteReturn>;
+
+export function makePipe<
+ TRead,
+ TWrite = unknown,
+ TReadReturn = unknown,
+ TWriteReturn = unknown,
+>(): [
+ Stream<TRead, TWrite, TReadReturn, TWriteReturn>,
+ Stream<TWrite, TRead, TWriteReturn, TReadReturn>,
+];

MODIFY
packages/stream/index.js
packages/stream/index.js
* }} PromiseKit
*/
+// TypeScript ReadOnly semantics are not sufficiently expressive to distinguish
+// a value one promises not to alter from a value one must not alter,
+// making it useless.
+const freeze = /** @type {<T>(v: T | Readonly<T>) => T} */ (Object.freeze);
+
/**
* @template T
* @returns {PromiseKit<T>}
@@ -38,22 +43,14 @@ const makePromiseKit = () => {
/**
* @template T
- * @typedef {{
- * put(value: T | Promise<T>): void,
- * get(): Promise<T>
- * }} AsyncQueue
- */
-
-/**
- * @template T
- * @returns {AsyncQueue<T>}
+ * @returns {import('./types.js').AsyncQueue<T>}
*/
export const makeQueue = () => {
let { promise: tailPromise, resolve: tailResolve } = makePromiseKit();
return {
put(value) {
const { resolve, promise } = makePromiseKit();
- tailResolve(Object.freeze({ value, promise }));
+ tailResolve(freeze({ value, promise }));
tailResolve = resolve;
},
get() {
@@ -66,53 +63,29 @@ export const makeQueue = () => {
harden(makeQueue);
/**
- * @template T
- * @template U
- * @template V
- * @typedef {{
- * next(value: U): Promise<IteratorResult<T>>,
- * return(value: V): Promise<IteratorResult<T>>,
- * throw(error: Error): Promise<IteratorResult<T>>,
- * [Symbol.asyncIterator](): Stream<T, U, V>
- * }} Stream
- */
-
-/**
- * @template T
- * @template V
- * @typedef {Stream<T, undefined, V>} Reader
- */
-
-/**
- * @template U
- * @template V
- * @typedef {Stream<undefined, U, V>} Writer
- */
-
-/**
- * @template T
- * @template U
- * @template V
- * @param {AsyncQueue<IteratorResult<T>>} acks
- * @param {AsyncQueue<IteratorResult<U>>} data
- * @returns {Stream<T, U, V>}
+ * @template TRead
+ * @template TWrite
+ * @template TReadReturn
+ * @template TWriteReturn
+ * @param {import('./types.js').AsyncQueue<IteratorResult<TRead, TReadReturn>>} acks
+ * @param {import('./types.js').AsyncQueue<IteratorResult<TWrite, TWriteReturn>>} data
*/
export const makeStream = (acks, data) => {
const stream = harden({
/**
- * @param {U} value
+ * @param {TWrite} value
*/
next(value) {
// Note the shallow freeze since value is not guaranteed to be freezable
// (typed arrays are not).
- data.put(Object.freeze({ value, done: false }));
+ data.put(freeze({ value, done: false }));
return acks.get();
},
/**
- * @param {V} value
+ * @param {TWriteReturn} value
*/
return(value) {
- data.put(Object.freeze({ value, done: true }));
+ data.put(freeze({ value, done: true }));
return acks.get();
},
/**
@@ -130,18 +103,14 @@ export const makeStream = (acks, data) => {
};
harden(makeStream);
-/**
- * @template T
- * @template U
- * @template TReturn
- * @template UReturn
- * @returns {[Stream<T, U, TReturn>, Stream<U, T, UReturn>]}
- */
+// JSDoc TypeScript seems unable to express this particular function's
+// entanglement of queues, but the definition in index.d.ts works for the end
+// user.
export const makePipe = () => {
const data = makeQueue();
const acks = makeQueue();
- const reader = makeStream(data, acks);
- const writer = makeStream(acks, data);
- return harden([reader, writer]);
+ const reader = makeStream(acks, data);
+ const writer = makeStream(data, acks);
+ return harden([writer, reader]);
};
harden(makePipe);

MODIFY
packages/stream/jsconfig.json
packages/stream/jsconfig.json
"strictNullChecks": true,
"moduleResolution": "node"
},
- "include": ["src/**/*.js", "index.d.ts"]
+ "include": ["*.js", "*.ts"]
}

MODIFY
packages/stream/package.json
packages/stream/package.json
"files": [
"LICENSE*",
"index.d.ts",
- "index.js"
+ "index.js",
+ "types.d.ts"
],
"publishConfig": {
"access": "public"

MODIFY
packages/stream/test/test-stream.js
packages/stream/test/test-stream.js
+// @ts-check
+
import 'ses';
import './lockdown.js';
@@ -7,7 +9,7 @@ import { makePipe } from '../index.js';
const test = wrapTest(rawTest);
-test('stream', async t => {
+test('stream', async (/** @type {import('ava').Assertions} */ t) => {
const [consumeFrom, produceTo] = makePipe();
const order = [10, 20, 30];
@@ -39,7 +41,7 @@ test('stream', async t => {
await Promise.all([makeProducer(), makeConsumer()]);
});
-test('stream terminated with cause', async t => {
+test('stream terminated with cause', async (/** @type {import('ava').Assertions} */ t) => {
const [consumeFrom, produceTo] = makePipe();
const makeProducer = async () => {

ADD
packages/stream/types.d.ts
+export interface AsyncQueue<TValue> {
+ put(value: TValue | Promise<TValue>): void,
+ get(): Promise<TValue>,
+}
+
+// Stream is nearly identical to AsyncGenerator and AsyncGenerator should
+// probably be identical to this definition of Stream.
+// Stream does not make the mistake of conflating the read and write return
+// types.
+export interface Stream<
+TRead,
+TWrite = unknown,
+TReadReturn = unknown,
+TWriteReturn = unknown,
+> {
+ next(value: TWrite): Promise<IteratorResult<TRead, TReadReturn>>,
+ return(value: TWriteReturn): Promise<IteratorResult<TRead, TReadReturn>>,
+ throw(error: Error): Promise<IteratorResult<TRead, TReadReturn>>,
+ [Symbol.asyncIterator](): Stream<TRead, TWrite, TReadReturn, TWriteReturn>,
+}
+
+export type Reader<TRead, TReadReturn = undefined> = Stream<TRead, unknown, TReadReturn, unknown>;
+export type Writer<TWrite, TWriteReturn = undefined> = Stream<unknown, TWrite, unknown, TWriteReturn>;

refactor(stream): Give types unto TypeScript