@sproutboat/toolchain 0.4.18 → 0.4.20

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,17 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.4.20
4
+
5
+ ### Patch Changes
6
+
7
+ - 0cdd803: `String.prototype.replaceAll` is linear instead of quadratic (baronunread/sproutboat#237). Porffor grew the result with one full-copy `strcat` per match; a patch to `compiler/builtins/string.ts` collects the pieces and joins them once. Replacing in a 2.1 MB string went from about 42 s to 71 ms (Node: 36 ms), with identical output.
8
+
9
+ ## 0.4.19
10
+
11
+ ### Patch Changes
12
+
13
+ - 872198d: Standalone binaries accept HTTP/1.0 requests instead of answering 505 (baronunread/sproutboat#236), so they work behind nginx's default `proxy_http_version 1.0`. uWebSockets only parsed `HTTP/1.1` request lines. A helper now edits its `HttpParser.h` before each native-fetch compile to accept `HTTP/1.0` and mark the request as ancient, which makes uWebSockets close the connection after the response, as HTTP/1.0 expects.
14
+
3
15
  ## 0.4.18
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.18",
3
+ "version": "0.4.20",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -30,6 +30,8 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
30
30
  "}\n" +
31
31
  "#define PORF_CORO_STACK_SIZE (256u * 1024u)\n",
32
32
  "compiler/index.js":
33
+ "import { hashId } from './modules.js';\n" +
34
+ " const uwsDir = uwebsockets.ensureUWebSockets();\n" +
33
35
  " '-fno-ident', '-ffunction-sections', '-fdata-sections',\n ...darwinReleaseCompileArgs,\n uSocketsArchive,\n '-lm'\n",
34
36
  "compiler/precompile.js":
35
37
  "const fs = require('node:fs');\n" +
@@ -47,6 +49,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
47
49
  " len = i;\n" +
48
50
  " }\n\n arr.length = len;\n\n return new ${name}(arr);\n};\n",
49
51
  "compiler/uwebsockets.js":
52
+ 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n' +
50
53
  "static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;\n" +
51
54
  "int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);\n" +
52
55
  "static std::string_view lookup_status_line(i32 status) {\n" +
@@ -117,6 +120,12 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
117
120
  "}\n",
118
121
  "compiler/builtins/json.ts": "// sb_json_utf16_v1\n",
119
122
  "runtime/fetch-globals.js": "// sb_text_encoder_scalar_v1\n",
123
+ "compiler/builtins/string.ts":
124
+ "export const __Porffor_string_replaceAll = (str: any, searchValue: any, replaceValue: any) => {\n" +
125
+ " let out: any = __Porffor_string_emptyLike(str);\n let appendIndex: i32 = 0;\n let searchIndex: i32 = 0;\n let matched: boolean = false;\n" +
126
+ " out = __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, matchIndex));\n out = __Porffor_strcat(out, __Porffor_string_applyReplacer(str, match, matchIndex, replaceValue));\n" +
127
+ " if (!matched) return str;\n return __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, thisLen));\n};\n" +
128
+ "export const __String_prototype_replaceAll = function (this: string, searchValue: any, replaceValue: any) {};\n",
120
129
  "compiler/builtins/promise.ts":
121
130
  "export const __Porffor_promise_resolve = (value: any, promise: any): void => {\n" +
122
131
  " if (Porffor.type(value) == Porffor.TYPES.object) {\n" +
@@ -199,6 +208,15 @@ test("the default call (no url/expectedSha256 override) never touches the networ
199
208
  },
200
209
  });
201
210
  expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
211
+ // #237: replaceAll joins its pieces once instead of a strcat per match.
212
+ expect(await readFile(join(dir, "compiler/builtins/string.ts"), "utf8")).toContain(
213
+ "return Porffor.callThis(__Array_prototype_join, pieces, '');",
214
+ );
215
+ // #236: the native-fetch build edits uWebSockets to accept HTTP/1.0.
216
+ const index = await readFile(join(dir, "compiler/index.js"), "utf8");
217
+ expect(index).toContain("import { sbPatchHttp10 } from './sb-http10.js';");
218
+ expect(index).toContain("const uwsDir = uwebsockets.ensureUWebSockets();\n sbPatchHttp10(uwsDir);");
219
+ expect(await readFile(join(dir, "compiler/sb-http10.js"), "utf8")).toContain("export const sbPatchHttp10");
202
220
  // #235: every compiled unit keeps JavaScript's separate float roundings.
203
221
  expect(await readFile(join(dir, "compiler/index.js"), "utf8")).toContain("'-ffp-contract=off',");
204
222
  // #168: the real vendored source takes the null guard on the `then` probe.
package/src/patch.test.ts CHANGED
@@ -44,7 +44,9 @@ int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_
44
44
  // body limit (#56), the #156 status-line switch, #163's collect_headers, and
45
45
  // #176's read_value forward decl / is_forbidden_response_header /
46
46
  // write_response_value.
47
- const SHIM = `static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;
47
+ const SHIM = `export const makeUWebSocketsShimSource = () => \`
48
+ #include "App.h"
49
+ static const size_t REQUEST_BODY_MAX_BYTES = 1024u * 1024u;
48
50
 
49
51
  int porf_native_fetch_read_value(struct jsval value, const char** out_buf, size_t* out_len, char** out_owned);
50
52
 
package/src/patch.ts CHANGED
@@ -200,6 +200,61 @@ const FP_CONTRACT_ANCHOR = " '-fno-ident', '-ffunction-sections', '-fdata-s
200
200
  const FP_CONTRACT_INJECT = " '-ffp-contract=off', // sproutboat #235: JS rounds a * b + c twice\n";
201
201
  const FP_CONTRACT_MARKER = "sproutboat #235";
202
202
 
203
+ // baronunread/sproutboat#236: uWebSockets answers any request line that isn't
204
+ // HTTP/1.1 with 505, so a standalone sprout behind nginx's default
205
+ // `proxy_http_version 1.0` failed every request. Its HttpRequest already has
206
+ // isAncient(), which closes the connection after the response; the parser just
207
+ // never sets it. uWebSockets is header-only and lives in Porffor's deps cache,
208
+ // not in the Porffor checkout, so a helper written next to compiler/index.js
209
+ // edits HttpParser.h right before each native-fetch compile (marker-guarded).
210
+ const HTTP10_HELPER = String.raw`// sproutboat #236: accept HTTP/1.0 request lines in uWebSockets.
211
+ import fs from 'node:fs';
212
+
213
+ const MARK = 'sb_http10_request';
214
+ const edits = [
215
+ [
216
+ 'struct HttpParser {',
217
+ '/* sproutboat #236: set by consumeRequestLine for an HTTP/1.0 request line. */\ninline thread_local bool sb_http10_request = false;\n\nstruct HttpParser {'
218
+ ],
219
+ [
220
+ '/* Scan until single SP, assume next is / (origin request) */\n char *start = data;',
221
+ '/* Scan until single SP, assume next is / (origin request) */\n sb_http10_request = false;\n char *start = data;'
222
+ ],
223
+ [
224
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }',
225
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }\n if (memcmp(" HTTP/1.0\\r\\n", data, std::min<unsigned int>(11, (unsigned int) (end - data))) == 0) {\n return nullptr;\n }'
226
+ ],
227
+ [
228
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, 11) == 0) {\n return data + 11;\n }',
229
+ 'if (memcmp(" HTTP/1.1\\r\\n", data, 11) == 0) {\n return data + 11;\n }\n if (memcmp(" HTTP/1.0\\r\\n", data, 11) == 0) {\n sb_http10_request = true;\n return data + 11;\n }'
230
+ ],
231
+ [ 'req->ancientHttp = false;', 'req->ancientHttp = sb_http10_request;' ]
232
+ ];
233
+
234
+ export const sbPatchHttp10 = uwsDir => {
235
+ const file = uwsDir + '/src/HttpParser.h';
236
+ let src = fs.readFileSync(file, 'utf8');
237
+ if (src.includes(MARK)) return;
238
+ for (const [ from, to ] of edits) {
239
+ if (!src.includes(from)) throw new Error('sproutboat #236: uWebSockets HttpParser.h changed, anchor not found: ' + from.slice(0, 60));
240
+ src = src.replace(from, to);
241
+ }
242
+ fs.writeFileSync(file, src);
243
+ };
244
+ `;
245
+ const HTTP10_IMPORT_ANCHOR = "import { hashId } from './modules.js';\n";
246
+ const HTTP10_IMPORT_INJECT = "import { sbPatchHttp10 } from './sb-http10.js'; // sproutboat #236\n";
247
+ const HTTP10_IMPORT_MARKER = "./sb-http10.js";
248
+ const HTTP10_CALL_ANCHOR = " const uwsDir = uwebsockets.ensureUWebSockets();\n";
249
+ const HTTP10_CALL_INJECT = " sbPatchHttp10(uwsDir);\n";
250
+ const HTTP10_CALL_MARKER = "sbPatchHttp10(uwsDir)";
251
+ // The shim is content-cached, so a header edit alone would never recompile it.
252
+ // This line changes the shim once, which rebuilds it against the edited header.
253
+ const HTTP10_SHIM_ANCHOR = 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n';
254
+ const HTTP10_SHIM_INJECT =
255
+ 'export const makeUWebSocketsShimSource = () => `\n#include "App.h"\n// sproutboat #236: built against an HttpParser.h that accepts HTTP/1.0\n';
256
+ const HTTP10_SHIM_MARKER = "sproutboat #236: built against";
257
+
203
258
  // baronunread/sproutboat#168: Porffor represents null as an object-typed value
204
259
  // with pointer 0, so __Porffor_promise_resolve's cheap `then` probe treated a
205
260
  // promise resolving to null as an object and looked `then` up at memory offset
@@ -215,6 +270,27 @@ const PROMISE_NULL_INJECT =
215
270
  " if (Porffor.type(value) == Porffor.TYPES.object && value != null) {\n // cheap prototype-chain probe for 'then'";
216
271
  const PROMISE_NULL_MARKER = "sproutboat #168: null is object-typed";
217
272
 
273
+ // baronunread/sproutboat#237: String.prototype.replaceAll grew its result with
274
+ // one __Porffor_strcat per match, and each strcat copies everything so far, so
275
+ // a 2.7 MB string with many matches took ~22 s (Node: 17 ms). Collect the
276
+ // pieces and join once: Array.prototype.join sizes the result up front and
277
+ // copies each piece a single time.
278
+ const REPLACE_ALL_EDITS = [
279
+ [
280
+ " let out: any = __Porffor_string_emptyLike(str);\n let appendIndex: i32 = 0;\n let searchIndex: i32 = 0;\n let matched: boolean = false;",
281
+ " // sproutboat #237: pieces joined once, not a strcat (full copy) per match.\n const pieces: any[] = Porffor.array.new(0);\n let appendIndex: i32 = 0;\n let searchIndex: i32 = 0;\n let matched: boolean = false;",
282
+ ],
283
+ [
284
+ " out = __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, matchIndex));\n out = __Porffor_strcat(out, __Porffor_string_applyReplacer(str, match, matchIndex, replaceValue));",
285
+ " Porffor.array.fastPush(pieces, __Porffor_string_substringLike(str, appendIndex, matchIndex));\n Porffor.array.fastPush(pieces, __Porffor_string_applyReplacer(str, match, matchIndex, replaceValue));",
286
+ ],
287
+ [
288
+ " if (!matched) return str;\n return __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, thisLen));\n};",
289
+ " if (!matched) return str;\n Porffor.array.fastPush(pieces, __Porffor_string_substringLike(str, appendIndex, thisLen));\n return Porffor.callThis(__Array_prototype_join, pieces, '');\n};",
290
+ ],
291
+ ] as const;
292
+ const REPLACE_ALL_MARKER = "sproutboat #237: pieces joined once";
293
+
218
294
  // Porffor's TypedArray.from only handles iterables. An array-like input such
219
295
  // as { length: 16 } silently becomes an empty typed array, including HMAC
220
296
  // keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
@@ -775,6 +851,7 @@ const done = new Set<string>();
775
851
 
776
852
  /** Edits to Porffor's uWebSockets shim: `[marker, anchor, inject, what]`. */
777
853
  const UWS_EDITS = [
854
+ [HTTP10_SHIM_MARKER, HTTP10_SHIM_ANCHOR, HTTP10_SHIM_INJECT, "shim rebuild for HTTP/1.0 (#236)"],
778
855
  [BODY_MARKER, BODY_ANCHOR, BODY_INJECT, "request body limit"],
779
856
  [STATUS_SIG_MARKER, STATUS_SIG_ANCHOR, STATUS_SIG_INJECT, "status-line return type (#156)"],
780
857
  [STATUS_303_MARKER, STATUS_303_ANCHOR, STATUS_303_INJECT, "status-line 303 case (#156)"],
@@ -961,6 +1038,7 @@ export async function patchUwebsockets(root: string): Promise<void> {
961
1038
  }
962
1039
 
963
1040
  async function patchCompilerArgs(root: string): Promise<void> {
1041
+ await writeFile(resolve(root, "compiler/sb-http10.js"), HTTP10_HELPER);
964
1042
  const file = resolve(root, "compiler/index.js");
965
1043
  let src = await readFile(file, "utf8");
966
1044
  let changed = false;
@@ -968,6 +1046,8 @@ async function patchCompilerArgs(root: string): Promise<void> {
968
1046
  [LINK_MARKER, LINK_ANCHOR, LINK_INJECT, "extra link args"],
969
1047
  [CFLAGS_MARKER, CFLAGS_ANCHOR, CFLAGS_INJECT, "extra compiler flags"],
970
1048
  [FP_CONTRACT_MARKER, FP_CONTRACT_ANCHOR, FP_CONTRACT_INJECT, "floating-point contraction"],
1049
+ [HTTP10_IMPORT_MARKER, HTTP10_IMPORT_ANCHOR, HTTP10_IMPORT_INJECT, "HTTP/1.0 helper import (#236)"],
1050
+ [HTTP10_CALL_MARKER, HTTP10_CALL_ANCHOR, HTTP10_CALL_INJECT, "HTTP/1.0 parser edit (#236)"],
971
1051
  ] as const) {
972
1052
  if (src.includes(marker)) continue;
973
1053
  const at = src.indexOf(anchor);
@@ -1011,6 +1091,23 @@ export async function patchPromiseResolveNull(root: string): Promise<void> {
1011
1091
  await writeFile(file, src.replace(PROMISE_NULL_ANCHOR, PROMISE_NULL_INJECT));
1012
1092
  }
1013
1093
 
1094
+ /** #237: linear String.prototype.replaceAll. Precompiled by patchTypedArrayFrom. */
1095
+ export async function patchReplaceAll(root: string): Promise<void> {
1096
+ const file = resolve(root, "compiler/builtins/string.ts");
1097
+ let src = await readFile(file, "utf8");
1098
+ if (src.includes(REPLACE_ALL_MARKER)) return;
1099
+ const start = src.indexOf("export const __Porffor_string_replaceAll = ");
1100
+ const end = src.indexOf("export const __String_prototype_replaceAll = ", start);
1101
+ if (start < 0 || end < 0) throw new Error(`could not find Porffor's replaceAll in ${file}`);
1102
+ let body = src.slice(start, end);
1103
+ for (const [anchor, inject] of REPLACE_ALL_EDITS) {
1104
+ if (!body.includes(anchor)) throw new Error(`could not patch Porffor's replaceAll: anchor not found in ${file}`);
1105
+ body = body.replace(anchor, inject);
1106
+ }
1107
+ src = src.slice(0, start) + body + src.slice(end);
1108
+ await writeFile(file, src);
1109
+ }
1110
+
1014
1111
  export async function patchTypedArrayFrom(root: string): Promise<void> {
1015
1112
  const file = resolve(root, "compiler/builtins/typedarray.js");
1016
1113
  const src = await readFile(file, "utf8");
@@ -1087,8 +1184,9 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
1087
1184
  await patchRenderJs(root);
1088
1185
  await patchDateParser(root);
1089
1186
  await patchPromiseResolveNull(root);
1090
- // Last of the builtin edits: its precompile picks up the date and promise
1091
- // patches above too.
1187
+ await patchReplaceAll(root);
1188
+ // Last of the builtin edits: its precompile picks up the date, promise and
1189
+ // replaceAll patches above too.
1092
1190
  await patchTypedArrayFrom(root);
1093
1191
  await patchUnicode(root);
1094
1192
  done.add(root);