turbo-stream 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,3 +24,5 @@ console.log(decoded.value); // a Promise
24
24
  console.log(await decoded.value); // 42
25
25
  await decoded.done; // wait for the stream to finish
26
26
  ```
27
+
28
+ Stackblitz: https://stackblitz.com/edit/stackblitz-starters-2wm7dh?file=index.js
@@ -0,0 +1,2 @@
1
+ import { type ThisEncode } from "./utils.js";
2
+ export declare function flatten(this: ThisEncode, input: unknown): number;
@@ -1,4 +1,4 @@
1
- import { HOLE, NAN, NEGATIVE_INFINITY, NEGATIVE_ZERO, POSITIVE_INFINITY, UNDEFINED, TYPE_BIGINT, TYPE_DATE, TYPE_MAP, TYPE_REGEXP, TYPE_SET, TYPE_SYMBOL, TYPE_PROMISE, } from "./constants.js";
1
+ import { HOLE, NAN, NEGATIVE_INFINITY, NEGATIVE_ZERO, POSITIVE_INFINITY, TYPE_BIGINT, TYPE_DATE, TYPE_MAP, TYPE_PROMISE, TYPE_REGEXP, TYPE_SET, TYPE_SYMBOL, UNDEFINED, } from "./utils.js";
2
2
  export function flatten(input) {
3
3
  if (this.indicies.has(input)) {
4
4
  return this.indicies.get(input);
@@ -81,11 +81,10 @@ function stringify(input, index) {
81
81
  }
82
82
  if (input instanceof Promise) {
83
83
  this.stringified[index] = `["${TYPE_PROMISE}",${index}]`;
84
- this.deferred.push([index, input]);
84
+ this.deferred[index] = input;
85
85
  break;
86
86
  }
87
87
  if (!isPlainObject(input)) {
88
- console.log(input);
89
88
  throw new Error("Cannot encode object with prototype");
90
89
  }
91
90
  let result = "{";
@@ -1,5 +1,5 @@
1
- export declare function decode<T = unknown>(input: ReadableStream<Uint8Array>): Promise<{
2
- value: T;
3
- done: Promise<void>;
1
+ export declare function decode(readable: ReadableStream<Uint8Array>): Promise<{
2
+ done: Promise<undefined>;
3
+ value: unknown;
4
4
  }>;
5
5
  export declare function encode(input: unknown): ReadableStream<Uint8Array>;
@@ -1,121 +1,144 @@
1
- import { TYPE_PROMISE } from "./constants.js";
2
- import { unflatten } from "./decode.js";
3
- import { Deferred } from "./deferred.js";
4
- import { flatten } from "./encode.js";
5
- export async function decode(input) {
6
- const decoder = new Decoder(input);
7
- return decoder.decode();
1
+ import { flatten } from "./flatten.js";
2
+ import { unflatten } from "./unflatten.js";
3
+ import { createLineSplittingTransform, Deferred, TYPE_PROMISE, } from "./utils.js";
4
+ export async function decode(readable) {
5
+ const done = new Deferred();
6
+ const reader = readable
7
+ .pipeThrough(createLineSplittingTransform())
8
+ .getReader();
9
+ const decoder = {
10
+ values: [],
11
+ hydrated: [],
12
+ deferred: {},
13
+ };
14
+ const decoded = await decodeInitial.call(decoder, reader);
15
+ let donePromise = done.promise;
16
+ if (decoded.done) {
17
+ done.resolve();
18
+ }
19
+ else {
20
+ donePromise = decodeDeferred
21
+ .call(decoder, reader)
22
+ .then(done.resolve)
23
+ .catch((reason) => {
24
+ for (const deferred of Object.values(decoder.deferred)) {
25
+ deferred.reject(reason);
26
+ }
27
+ done.reject(reason);
28
+ });
29
+ }
30
+ return {
31
+ done: donePromise.then(() => reader.closed),
32
+ value: decoded.value,
33
+ };
8
34
  }
9
- class Decoder {
10
- reader;
11
- decoder;
12
- constructor(input) {
13
- this.reader = input.getReader();
14
- this.decoder = {
15
- deferred: {},
16
- hydrated: [],
17
- values: [],
18
- };
35
+ class SyntaxError extends Error {
36
+ name = "SyntaxError";
37
+ constructor(message) {
38
+ super(message ?? `Invalid input`);
39
+ }
40
+ }
41
+ async function decodeInitial(reader) {
42
+ const read = await reader.read();
43
+ if (!read.value) {
44
+ throw new SyntaxError();
45
+ }
46
+ let line;
47
+ try {
48
+ line = JSON.parse(read.value);
49
+ }
50
+ catch (reason) {
51
+ throw new SyntaxError();
19
52
  }
20
- async decode() {
21
- const iterator = makeTextFileLineIterator(this.reader);
22
- const read = await iterator.next();
23
- if (!read.value || read.done)
24
- throw new Error("Invalid input");
25
- const decoded = unflatten.call(this.decoder, JSON.parse(read.value));
26
- const done = (async () => {
27
- for await (const line of iterator) {
28
- let type = line[0];
29
- switch (type) {
30
- case TYPE_PROMISE:
31
- const colonIndex = line.indexOf(":");
32
- const deferredId = Number(line.slice(1, colonIndex));
33
- const lineData = line.slice(colonIndex + 1);
34
- const deferredResult = unflatten.call(this.decoder, JSON.parse(lineData));
35
- this.decoder.deferred[deferredId].resolve(deferredResult);
36
- break;
37
- default:
38
- throw new Error("Invalid input");
53
+ return {
54
+ done: read.done,
55
+ value: unflatten.call(this, line),
56
+ };
57
+ }
58
+ async function decodeDeferred(reader) {
59
+ let read = await reader.read();
60
+ while (!read.done) {
61
+ if (!read.value)
62
+ continue;
63
+ const line = read.value;
64
+ switch (line[0]) {
65
+ case TYPE_PROMISE:
66
+ const colonIndex = line.indexOf(":");
67
+ const deferredId = Number(line.slice(1, colonIndex));
68
+ const deferred = this.deferred[deferredId];
69
+ if (!deferred) {
70
+ throw new Error(`Deferred ID ${deferredId} not found in stream`);
39
71
  }
40
- }
41
- })();
42
- return { value: decoded, done };
72
+ const lineData = line.slice(colonIndex + 1);
73
+ let jsonLine;
74
+ try {
75
+ jsonLine = JSON.parse(lineData);
76
+ }
77
+ catch (reason) {
78
+ throw new SyntaxError();
79
+ }
80
+ const value = unflatten.call(this, jsonLine);
81
+ deferred.resolve(value);
82
+ break;
83
+ // case TYPE_PROMISE_ERROR:
84
+ // // TODO: transport promise rejections
85
+ // break;
86
+ default:
87
+ throw new SyntaxError();
88
+ }
89
+ read = await reader.read();
43
90
  }
44
91
  }
45
92
  export function encode(input) {
46
- return new ReadableStream({
93
+ const encoder = {
94
+ deferred: {},
95
+ index: 0,
96
+ indicies: new Map(),
97
+ stringified: [],
98
+ };
99
+ const textEncoder = new TextEncoder();
100
+ let lastSentIndex = 0;
101
+ const readable = new ReadableStream({
47
102
  async start(controller) {
48
- const textEncoder = new TextEncoder();
49
- const encoder = {
50
- index: 0,
51
- indicies: new Map(),
52
- stringified: [],
53
- deferred: [],
54
- };
55
- const id = flatten.call(encoder, input);
56
- const encoded = id < 0 ? String(id) : "[" + encoder.stringified.join(",") + "]";
57
- controller.enqueue(textEncoder.encode(encoded + "\n"));
58
- let activeDeferred = 0;
59
- const done = new Deferred();
60
- let alreadyDone = false;
61
- if (encoder.deferred.length === 0) {
62
- alreadyDone = true;
63
- done.resolve();
103
+ const id = flatten.call(encoder, await input);
104
+ if (id < 0) {
105
+ controller.enqueue(textEncoder.encode(`${id}\n`));
64
106
  }
65
107
  else {
66
- for (const [promiseId, promise] of encoder.deferred) {
67
- activeDeferred++;
68
- promise
69
- .then((value) => {
70
- const id = flatten.call(encoder, value);
71
- const encoded = id < 0
72
- ? String(id)
73
- : "[" + encoder.stringified.slice(id).join(",") + "]";
74
- controller.enqueue(textEncoder.encode(`${TYPE_PROMISE}${promiseId}:` + encoded + "\n"));
75
- activeDeferred--;
76
- if (activeDeferred === 0) {
77
- alreadyDone = true;
78
- done.resolve();
108
+ controller.enqueue(textEncoder.encode(`[${encoder.stringified.join(",")}]\n`));
109
+ lastSentIndex = encoder.stringified.length - 1;
110
+ }
111
+ const seenPromises = new WeakSet();
112
+ while (Object.keys(encoder.deferred).length > 0) {
113
+ for (const [deferredId, deferred] of Object.entries(encoder.deferred)) {
114
+ if (seenPromises.has(deferred))
115
+ continue;
116
+ seenPromises.add((encoder.deferred[Number(deferredId)] = deferred
117
+ .then((resolved) => {
118
+ const id = flatten.call(encoder, resolved);
119
+ if (id < 0) {
120
+ controller.enqueue(textEncoder.encode(`${TYPE_PROMISE}${deferredId}:${id}\n`));
79
121
  }
122
+ else {
123
+ const values = encoder.stringified
124
+ .slice(lastSentIndex + 1)
125
+ .join(",");
126
+ controller.enqueue(textEncoder.encode(`${TYPE_PROMISE}${deferredId}:[${values}]\n`));
127
+ lastSentIndex = encoder.stringified.length - 1;
128
+ }
129
+ }, (reason) => {
130
+ // TODO: Encode and send errors
131
+ throw reason;
80
132
  })
81
- .catch((reason) => {
82
- if (alreadyDone)
83
- return;
84
- alreadyDone = true;
85
- done.reject(reason);
86
- });
133
+ .finally(() => {
134
+ delete encoder.deferred[Number(deferredId)];
135
+ })));
87
136
  }
137
+ await Promise.race(Object.values(encoder.deferred));
88
138
  }
89
- await done.promise;
139
+ await Promise.all(Object.values(encoder.deferred));
90
140
  controller.close();
91
141
  },
92
142
  });
93
- }
94
- async function* makeTextFileLineIterator(reader) {
95
- const decoder = new TextDecoder();
96
- let read = await reader.read();
97
- let chunk = read.value ? decoder.decode(read.value, { stream: true }) : "";
98
- let re = /\r\n|\n|\r/gm;
99
- let startIndex = 0;
100
- for (;;) {
101
- let result = re.exec(chunk);
102
- if (!result) {
103
- if (read.done) {
104
- break;
105
- }
106
- let remainder = chunk.slice(startIndex);
107
- read = await reader.read();
108
- chunk =
109
- remainder +
110
- (read.value ? decoder.decode(read.value, { stream: true }) : "");
111
- startIndex = re.lastIndex = 0;
112
- continue;
113
- }
114
- yield chunk.substring(startIndex, result.index);
115
- startIndex = re.lastIndex;
116
- }
117
- if (startIndex < chunk.length) {
118
- // last line didn't end in a newline char
119
- yield chunk.slice(startIndex);
120
- }
143
+ return readable;
121
144
  }
@@ -0,0 +1,2 @@
1
+ import { type ThisDecode } from "./utils.js";
2
+ export declare function unflatten(this: ThisDecode, parsed: unknown): unknown;
@@ -1,5 +1,4 @@
1
- import { HOLE, NAN, NEGATIVE_INFINITY, NEGATIVE_ZERO, POSITIVE_INFINITY, UNDEFINED, TYPE_BIGINT, TYPE_DATE, TYPE_MAP, TYPE_REGEXP, TYPE_SET, TYPE_SYMBOL, TYPE_PROMISE, } from "./constants.js";
2
- import { Deferred } from "./deferred.js";
1
+ import { Deferred, HOLE, NAN, NEGATIVE_INFINITY, NEGATIVE_ZERO, POSITIVE_INFINITY, TYPE_BIGINT, TYPE_DATE, TYPE_MAP, TYPE_PROMISE, TYPE_REGEXP, TYPE_SET, TYPE_SYMBOL, UNDEFINED, } from "./utils.js";
3
2
  export function unflatten(parsed) {
4
3
  if (typeof parsed === "number")
5
4
  return hydrate.call(this, parsed, true);
@@ -60,9 +59,14 @@ function hydrate(index, standalone) {
60
59
  }
61
60
  break;
62
61
  case TYPE_PROMISE:
63
- const deferred = new Deferred();
64
- this.deferred[value[1]] = deferred;
65
- this.hydrated[index] = deferred.promise;
62
+ if (this.hydrated[value[1]]) {
63
+ this.hydrated[index] = this.hydrated[value[1]];
64
+ }
65
+ else {
66
+ const deferred = new Deferred();
67
+ this.deferred[value[1]] = deferred;
68
+ this.hydrated[index] = deferred.promise;
69
+ }
66
70
  break;
67
71
  default:
68
72
  throw new Error(`Invalid input`);
@@ -12,3 +12,21 @@ export declare const TYPE_REGEXP = "R";
12
12
  export declare const TYPE_SYMBOL = "Y";
13
13
  export declare const TYPE_NULL_OBJECT = "N";
14
14
  export declare const TYPE_PROMISE = "P";
15
+ export interface ThisDecode {
16
+ values: unknown[];
17
+ hydrated: unknown[];
18
+ deferred: Record<number, Deferred<unknown>>;
19
+ }
20
+ export interface ThisEncode {
21
+ index: number;
22
+ indicies: Map<unknown, number>;
23
+ stringified: string[];
24
+ deferred: Record<number, Promise<unknown>>;
25
+ }
26
+ export declare class Deferred<T = unknown> {
27
+ promise: Promise<T>;
28
+ resolve: (value: T) => void;
29
+ reject: (reason: unknown) => void;
30
+ constructor();
31
+ }
32
+ export declare function createLineSplittingTransform(): TransformStream<any, any>;
package/dist/utils.js ADDED
@@ -0,0 +1,46 @@
1
+ export const UNDEFINED = -1;
2
+ export const HOLE = -1;
3
+ export const NAN = -2;
4
+ export const POSITIVE_INFINITY = -3;
5
+ export const NEGATIVE_INFINITY = -4;
6
+ export const NEGATIVE_ZERO = -5;
7
+ export const TYPE_BIGINT = "B";
8
+ export const TYPE_DATE = "D";
9
+ export const TYPE_MAP = "M";
10
+ export const TYPE_SET = "S";
11
+ export const TYPE_REGEXP = "R";
12
+ export const TYPE_SYMBOL = "Y";
13
+ export const TYPE_NULL_OBJECT = "N";
14
+ export const TYPE_PROMISE = "P";
15
+ export class Deferred {
16
+ promise;
17
+ resolve;
18
+ reject;
19
+ constructor() {
20
+ this.promise = new Promise((resolve, reject) => {
21
+ this.resolve = resolve;
22
+ this.reject = reject;
23
+ });
24
+ }
25
+ }
26
+ export function createLineSplittingTransform() {
27
+ let decoder = new TextDecoder();
28
+ let leftover = "";
29
+ return new TransformStream({
30
+ transform(chunk, controller) {
31
+ let str = decoder.decode(chunk, { stream: true });
32
+ let parts = (leftover + str).split("\n");
33
+ // The last part might be a partial line, so keep it for the next chunk.
34
+ leftover = parts.pop() || "";
35
+ for (const part of parts) {
36
+ controller.enqueue(part);
37
+ }
38
+ },
39
+ flush(controller) {
40
+ // If there's any leftover data, enqueue it before closing.
41
+ if (leftover) {
42
+ controller.enqueue(leftover);
43
+ }
44
+ },
45
+ });
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbo-stream",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A streaming data transport format that aims to support built-in features such as Promises, Dates, RegExps, Maps, Sets and more.",
5
5
  "type": "module",
6
6
  "files": [
@@ -14,6 +14,11 @@
14
14
  },
15
15
  "./package.json": "./package.json"
16
16
  },
17
+ "scripts": {
18
+ "build": "tsc --outDir dist --project tsconfig.lib.json",
19
+ "test": "node --no-warnings --loader tsm --enable-source-maps --test-reporter tap --test src/*.spec.ts",
20
+ "test-typecheck": "tsc --noEmit --project tsconfig.spec.json"
21
+ },
17
22
  "keywords": [],
18
23
  "author": "",
19
24
  "license": "ISC",
@@ -22,10 +27,5 @@
22
27
  "expect": "^29.7.0",
23
28
  "tsm": "^2.3.0",
24
29
  "typescript": "^5.2.2"
25
- },
26
- "scripts": {
27
- "build": "tsc --outDir dist --project tsconfig.lib.json",
28
- "test": "node --no-warnings --loader tsm --enable-source-maps --test-reporter tap --test src/*.spec.ts",
29
- "test-typecheck": "tsc --noEmit --project tsconfig.spec.json"
30
30
  }
31
- }
31
+ }
package/dist/constants.js DELETED
@@ -1,14 +0,0 @@
1
- export const UNDEFINED = -1;
2
- export const HOLE = -1;
3
- export const NAN = -2;
4
- export const POSITIVE_INFINITY = -3;
5
- export const NEGATIVE_INFINITY = -4;
6
- export const NEGATIVE_ZERO = -5;
7
- export const TYPE_BIGINT = "B";
8
- export const TYPE_DATE = "D";
9
- export const TYPE_MAP = "M";
10
- export const TYPE_SET = "S";
11
- export const TYPE_REGEXP = "R";
12
- export const TYPE_SYMBOL = "Y";
13
- export const TYPE_NULL_OBJECT = "N";
14
- export const TYPE_PROMISE = "P";
package/dist/decode.d.ts DELETED
@@ -1,7 +0,0 @@
1
- import { Deferred } from "./deferred.js";
2
- export interface ThisDecode {
3
- values: unknown[];
4
- hydrated: unknown[];
5
- deferred: Record<number, Deferred<unknown>>;
6
- }
7
- export declare function unflatten(this: ThisDecode, parsed: unknown): unknown;
@@ -1,6 +0,0 @@
1
- export declare class Deferred<R> {
2
- promise: Promise<R>;
3
- resolve: (value: R | PromiseLike<R>) => void;
4
- reject: (reason?: unknown) => void;
5
- constructor();
6
- }
package/dist/deferred.js DELETED
@@ -1,11 +0,0 @@
1
- export class Deferred {
2
- promise;
3
- resolve;
4
- reject;
5
- constructor() {
6
- this.promise = new Promise((resolve, reject) => {
7
- this.resolve = resolve;
8
- this.reject = reject;
9
- });
10
- }
11
- }
package/dist/encode.d.ts DELETED
@@ -1,7 +0,0 @@
1
- export interface ThisEncode {
2
- index: number;
3
- indicies: Map<unknown, number>;
4
- stringified: string[];
5
- deferred: [number, Promise<unknown>][];
6
- }
7
- export declare function flatten(this: ThisEncode, input: unknown): number;