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 +2 -0
- package/dist/flatten.d.ts +2 -0
- package/dist/{encode.js → flatten.js} +2 -3
- package/dist/turbo-stream.d.ts +3 -3
- package/dist/turbo-stream.js +127 -104
- package/dist/unflatten.d.ts +2 -0
- package/dist/{decode.js → unflatten.js} +9 -5
- package/dist/{constants.d.ts → utils.d.ts} +18 -0
- package/dist/utils.js +46 -0
- package/package.json +7 -7
- package/dist/constants.js +0 -14
- package/dist/decode.d.ts +0 -7
- package/dist/deferred.d.ts +0 -6
- package/dist/deferred.js +0 -11
- package/dist/encode.d.ts +0 -7
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HOLE, NAN, NEGATIVE_INFINITY, NEGATIVE_ZERO, POSITIVE_INFINITY,
|
|
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
|
|
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 = "{";
|
package/dist/turbo-stream.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare function decode
|
|
2
|
-
|
|
3
|
-
|
|
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>;
|
package/dist/turbo-stream.js
CHANGED
|
@@ -1,121 +1,144 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { unflatten } from "./
|
|
3
|
-
import { Deferred } from "./
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { HOLE, NAN, NEGATIVE_INFINITY, NEGATIVE_ZERO, POSITIVE_INFINITY,
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
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
package/dist/deferred.d.ts
DELETED
package/dist/deferred.js
DELETED