@sproutboat/toolchain 0.8.1 → 0.10.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4274bef: `Response.body` and `Request.body` are `ReadableStream`s for every body type, as in the Fetch spec and workerd. Before, they held the value given (a string, bytes or a `Blob`). `res.body.getReader()`, `res.body.pipeThrough(...)` and `new Response(other.body)` now work on any response, including the incoming request. The value as given stays internal, so responses are still written with a fixed length unless the body really is a stream. A body whose stream has been locked can no longer be read with `text()` and the other readers, which throw a `TypeError`.
8
+
9
+ ## 0.9.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 2541458: More of the Web platform, from sproutd's W2 conformance suite against workerd:
14
+
15
+ - `CompressionStream` and `DecompressionStream` for `gzip`, `deflate` and `deflate-raw`, in JavaScript, because the native runtime links no zlib. Output decompresses with zlib, and zlib's output (stored, fixed and dynamic blocks) decompresses here. A corrupt or truncated input errors with a `TypeError`.
16
+ - A `Response` or `Request` whose body is a `ReadableStream` is read to the end by `text()`, `json()`, `arrayBuffer()` and `blob()`. Before, these returned `"[object ReadableStream]"`. Chunks must be bytes, as in workerd.
17
+ - `JSON.parse` applies its reviver: children first, `undefined` deletes the property, and `this` is the holder.
18
+
3
19
  ## 0.8.1
4
20
 
5
21
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -151,6 +151,8 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
151
151
  "compiler/builtins/json.ts":
152
152
  "let __Porffor_json_bufLimit: i32 = 0;\n depth += 1;\n depth -= 1;\n depth += 1;\n depth -= 1;\n let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n" +
153
153
  "// sb_json_utf16_v1\n" +
154
+ // patchJsonReviver's anchor.
155
+ "export const __JSON_parse = (_: bytestring) => {\n};\n" +
154
156
  "export const __Porffor_json_serialize = (_buffer: i32, value: any): i32 => {\n" +
155
157
  " for (const x of (value as any[])) {\n }\n" +
156
158
  " const val: any = value[key];\n" +
@@ -65,7 +65,22 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
65
65
  static _consume(message) {
66
66
  if (message._bodyUsed === true) throw new TypeError('Body has already been used');
67
67
  message._bodyUsed = true;
68
- return message.body;
68
+ // A stream the body getter handed out is what the reader reads: a locked one throws.
69
+ return message._bodyStream != null ? message._bodyStream : message._bodyInit;
70
+ }
71
+
72
+ // body is a ReadableStream, as in Fetch: a stream body itself, else a stream made once over
73
+ // the bytes. The value as given stays in _bodyInit, which the native writer, streaming and
74
+ // the prelude read; assigning body (the native ingress does) sets it.
75
+ static _bodyAsStream(message) {
76
+ const init = message._bodyInit;
77
+ if (init == null) return null;
78
+ if (Request._isStream(init)) return init;
79
+ if (message._bodyStream == null) {
80
+ const bytes = Request._bodyBytes(init);
81
+ message._bodyStream = new globalThis.ReadableStream({ start(c) { if (bytes.length) c.enqueue(bytes); c.close(); } });
82
+ }
83
+ return message._bodyStream;
69
84
  }
70
85
 
71
86
  static _bodyText(body) {
@@ -74,6 +89,40 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
74
89
  return (Porffor.type(body) | 0b10000000) == Porffor.TYPES.bytestring ? body : String(body);
75
90
  }
76
91
 
92
+ // A ReadableStream body is read to its end, and its chunks must be bytes, as in Fetch and
93
+ // workerd. Readers of such a body answer with a promise; other bodies keep answering at
94
+ // once, since the prelude reads them synchronously. The prelude defines ReadableStream on
95
+ // globalThis after this file is compiled.
96
+ static _isStream(body) {
97
+ const Stream = globalThis.ReadableStream;
98
+ return Stream != null && body instanceof Stream;
99
+ }
100
+ static async _streamBytes(stream) {
101
+ const reader = stream.getReader();
102
+ const chunks = [];
103
+ let size = 0;
104
+ for (;;) {
105
+ const step = await reader.read();
106
+ if (step.done) break;
107
+ const value = step.value;
108
+ if (!(value instanceof Uint8Array)) throw new TypeError('This ReadableStream did not return bytes.');
109
+ chunks.push(value);
110
+ size += value.length;
111
+ }
112
+ const out = new Uint8Array(size);
113
+ let at = 0;
114
+ for (const chunk of chunks) {
115
+ out.set(chunk, at);
116
+ at += chunk.length;
117
+ }
118
+ return out;
119
+ }
120
+ static _read(message, finish) {
121
+ const body = Request._consume(message);
122
+ if (Request._isStream(body)) return Request._streamBytes(body).then(finish);
123
+ return finish(Request._bodyBytes(body));
124
+ }
125
+
77
126
  constructor(input, init = undefined) {
78
127
  let url = input;
79
128
  let method = 'GET';
@@ -85,7 +134,7 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
85
134
  url = input.url;
86
135
  method = input.method;
87
136
  headers = input.headers;
88
- body = input.body;
137
+ body = input._bodyStream != null ? input._bodyStream : input._bodyInit;
89
138
  redirect = input.redirect;
90
139
  } else if (input != null && Porffor.type(input) == Porffor.TYPES.object && input.href != null) url = input.href;
91
140
 
@@ -108,21 +157,31 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
108
157
  if (init != null && init.body != null && init.body !== '' && (this.method == 'GET' || this.method == 'HEAD'))
109
158
  throw new TypeError('Request with GET/HEAD method cannot have body.');
110
159
  this.headers = new Headers(headers);
111
- this.body = Request._extractBody(body, this.headers);
160
+ this._bodyInit = Request._extractBody(body, this.headers);
112
161
  if (redirect != null) this._redirect = String(redirect);
113
162
  }
114
163
 
115
164
  get redirect() { return this._redirect ?? 'follow'; }
165
+ get body() { return Request._bodyAsStream(this); }
166
+ set body(value) { this._bodyInit = value; this._bodyStream = null; }
116
167
  get bodyUsed() { return this._bodyUsed === true; }
117
168
 
118
- arrayBuffer() { return Request._bodyBytes(Request._consume(this)).buffer; }
119
- blob() { return new Blob([Request._bodyBytes(Request._consume(this))]); }
169
+ arrayBuffer() { return Request._read(this, (bytes) => bytes.buffer); }
170
+ blob() { return Request._read(this, (bytes) => new Blob([bytes])); }
120
171
  clone() {
121
172
  if (this._bodyUsed === true) throw new TypeError('Body has already been used');
122
173
  return new Request(this);
123
174
  }
124
- json() { return JSON.parse(Request._bodyText(Request._consume(this))); }
125
- text() { return Request._bodyText(Request._consume(this)); }
175
+ json() {
176
+ const body = Request._consume(this);
177
+ if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => JSON.parse(new TextDecoder().decode(bytes)));
178
+ return JSON.parse(Request._bodyText(body));
179
+ }
180
+ text() {
181
+ const body = Request._consume(this);
182
+ if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => new TextDecoder().decode(bytes));
183
+ return Request._bodyText(body);
184
+ }
126
185
  }
127
186
 
128
187
  `;
@@ -137,7 +196,7 @@ export const RESPONSE_CLASS_SOURCE = String.raw`class Response {
137
196
  throw new TypeError('Response with null body status cannot have body');
138
197
  if (init?.statusText != null) this._statusText = String(init.statusText);
139
198
  this.headers = new Headers(init?.headers);
140
- this.body = Request._extractBody(body, this.headers);
199
+ this._bodyInit = Request._extractBody(body, this.headers);
141
200
  }
142
201
 
143
202
  static error() {
@@ -150,20 +209,30 @@ export const RESPONSE_CLASS_SOURCE = String.raw`class Response {
150
209
  get statusText() { return this._statusText ?? ''; }
151
210
  get type() { return this._type ?? 'default'; }
152
211
  get ok() { return this.status >= 200 && this.status < 300; }
212
+ get body() { return Request._bodyAsStream(this); }
213
+ set body(value) { this._bodyInit = value; this._bodyStream = null; }
153
214
  get bodyUsed() { return this._bodyUsed === true; }
154
215
 
155
- arrayBuffer() { return Request._bodyBytes(Request._consume(this)).buffer; }
156
- blob() { return new Blob([Request._bodyBytes(Request._consume(this))]); }
216
+ arrayBuffer() { return Request._read(this, (bytes) => bytes.buffer); }
217
+ blob() { return Request._read(this, (bytes) => new Blob([bytes])); }
157
218
  clone() {
158
219
  if (this._bodyUsed === true) throw new TypeError('Body has already been used');
159
220
  const copy = new Response(null, { status: this.status == 0 ? 200 : this.status, statusText: this._statusText, headers: this.headers });
160
- copy.body = this.body;
221
+ copy._bodyInit = this._bodyInit;
161
222
  copy.status = this.status;
162
223
  if (this._type != null) copy._type = this._type;
163
224
  return copy;
164
225
  }
165
- json() { return JSON.parse(Request._bodyText(Request._consume(this))); }
166
- text() { return Request._bodyText(Request._consume(this)); }
226
+ json() {
227
+ const body = Request._consume(this);
228
+ if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => JSON.parse(new TextDecoder().decode(bytes)));
229
+ return JSON.parse(Request._bodyText(body));
230
+ }
231
+ text() {
232
+ const body = Request._consume(this);
233
+ if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => new TextDecoder().decode(bytes));
234
+ return Request._bodyText(body);
235
+ }
167
236
  }
168
237
  `;
169
238
 
@@ -173,7 +242,7 @@ export const RESPONSE_BYTES_ANCHOR =
173
242
  export const RESPONSE_BYTES_INJECT =
174
243
  " // sproutboat: byte bodies go out as their bytes. A Latin-1 string, one character per byte,\n" +
175
244
  " // built in windows (concatenating one long string is quadratic), sent raw by the writer.\n" +
176
- " let body = response.body;\n" +
245
+ " let body = response._bodyInit !== undefined ? response._bodyInit : response.body;\n" +
177
246
  " let headersEntries = response.headers._entries;\n" +
178
247
  " if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {\n" +
179
248
  " const view = body instanceof ArrayBuffer ? new Uint8Array(body) : new Uint8Array(body.buffer, body.byteOffset, body.byteLength);\n" +
@@ -21,7 +21,7 @@ export const PATCH_SOURCES = [
21
21
  * patch.ts as the module patch.ts is everywhere else. Kept current by fingerprint.test.ts
22
22
  * (UPDATE_FINGERPRINT=1 bun test packages/toolchain/src/fingerprint.test.ts).
23
23
  */
24
- export const RELEASED_FINGERPRINT = "f0ff426f97b0";
24
+ export const RELEASED_FINGERPRINT = "405c8f04fb7d";
25
25
 
26
26
  /** The fingerprint of the sources in dir, or null where they are not on disk. */
27
27
  export function sourceFingerprint(dir: string): string | null {
package/src/patch.ts CHANGED
@@ -1938,6 +1938,55 @@ export async function patchJsonCycles(root: string): Promise<void> {
1938
1938
  await writeFile(file, src);
1939
1939
  }
1940
1940
 
1941
+ // JSON.parse ignored its reviver (W2 conformance: json/reviver). The parser keeps its body
1942
+ // under a new name, and __JSON_parse walks the result as the spec's InternalizeJSONProperty
1943
+ // does: children first, a reviver answer of undefined deletes the property.
1944
+ const JSON_REVIVER_MARKER = "sproutboat: JSON.parse reviver";
1945
+ // Porffor declares (_: bytestring); patchUnicode widens it to any later, on the renamed parser.
1946
+ const JSON_PARSE_ANCHOR = /export const __JSON_parse = \(_: (bytestring|any)\) => \{\n/;
1947
+ const JSON_REVIVER_SOURCE = `
1948
+ // ${JSON_REVIVER_MARKER}
1949
+ export const __Porffor_json_revive = (holder: any, key: any, reviver: any): any => {
1950
+ const value: any = holder[key];
1951
+ if (Porffor.type(value) == Porffor.TYPES.array) {
1952
+ const len: i32 = value.length;
1953
+ for (let i: i32 = 0; i < len; i++) {
1954
+ const next: any = __Porffor_json_revive(value, ecma262.ToString(i), reviver);
1955
+ if (next === undefined) delete value[i];
1956
+ else value[i] = next;
1957
+ }
1958
+ } else if (Porffor.type(value) == Porffor.TYPES.object) {
1959
+ for (const k: any in value) {
1960
+ const next: any = __Porffor_json_revive(value, k, reviver);
1961
+ if (next === undefined) delete value[k];
1962
+ else value[k] = next;
1963
+ }
1964
+ }
1965
+ return reviver.call(holder, key, value);
1966
+ };
1967
+
1968
+ export const __JSON_parse = (text: any, reviver: any) => {
1969
+ const value: any = __Porffor_json_parseText(text);
1970
+ if (Porffor.type(reviver) != Porffor.TYPES.function) return value;
1971
+ const root: object = {};
1972
+ root[''] = value;
1973
+ return __Porffor_json_revive(root, '', reviver);
1974
+ };
1975
+ `;
1976
+
1977
+ /** JSON.parse applies its reviver, as InternalizeJSONProperty does. */
1978
+ export async function patchJsonReviver(root: string): Promise<void> {
1979
+ const file = resolve(root, "compiler/builtins/json.ts");
1980
+ const src = await readFile(file, "utf8");
1981
+ if (src.includes(JSON_REVIVER_MARKER)) return;
1982
+ if ((src.match(new RegExp(JSON_PARSE_ANCHOR, "g")) ?? []).length !== 1)
1983
+ throw new Error(`could not patch Porffor's JSON.parse reviver: anchor not found in ${file}`);
1984
+ await writeFile(
1985
+ file,
1986
+ src.replace(JSON_PARSE_ANCHOR, (_line, type) => `export const __Porffor_json_parseText = (_: ${type}) => {\n`).trimEnd() + "\n" + JSON_REVIVER_SOURCE,
1987
+ );
1988
+ }
1989
+
1941
1990
  // Object.prototype.toString gave "[object Object]" for typed arrays: their lazily
1942
1991
  // built prototypes have no toStringTag, and the fallback list of builtin tags left
1943
1992
  // them out (W2 conformance; V8 gives "[object Uint8Array]").
@@ -2251,6 +2300,7 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
2251
2300
  await patchPrivateFieldsHidden(root);
2252
2301
  await patchRepeatDoubling(root);
2253
2302
  await patchJsonCycles(root);
2303
+ await patchJsonReviver(root);
2254
2304
  await patchTypedArrayTag(root);
2255
2305
  await patchConsoleColors(root);
2256
2306
  await patchCaseMapping(root);
@@ -110,4 +110,6 @@ test("native Unicode JSON and UTF-8 encoding preserve code points and buffer bou
110
110
  }
111
111
  await rm(work, { recursive: true, force: true });
112
112
  }
113
- }, 60_000);
113
+ // An optimised native build of the whole prelude: about 17 s warm, but 50 to 110 s cold
114
+ // (a new patch fingerprint or prelude recompiles everything), which 60 s did not cover.
115
+ }, 180_000);