@sproutboat/toolchain 0.4.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - fa09d90: Update the pinned Porffor compiler to alpha 6 and remove the superseded local promise-resolution workaround.
8
+
9
+ ## 0.4.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 969add9: Fix (baronunread/sproutboat#168): a `fetch` handler that resolves a promise
14
+ with a plain object — the common shape for a JSON response — could wedge a
15
+ standalone/native-fetch build at 100% CPU after a handful of requests, with no
16
+ error and no log line. Root cause: `__Porffor_promise_resolve`'s duck-typing
17
+ probe for `.then` walks the value's prototype chain with no termination
18
+ guard, and under certain allocator conditions the walk hits a fixed point
19
+ instead of `null`/`undefined`, spinning forever.
20
+
21
+ `patchPromiseTs` now guards the probe the same way `_internal_object.ts`'s own
22
+ prototype-chain walks already do elsewhere in the runtime: track the previous
23
+ prototype and stop once the "next" pointer stops advancing, reading a
24
+ fixed-point chain as "no `.then` found" instead of spinning. Not specific to
25
+ `Date.prototype.toISOString()` — an earlier note wrongly correlated the bug
26
+ with that one call; see `sproutboat-cli/patches/UPSTREAM.md`.
27
+
3
28
  ## 0.4.0
4
29
 
5
30
  ### Minor Changes
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/baronunread/sproutboat-packages.git",
9
+ "directory": "packages/toolchain"
10
+ },
6
11
  "main": "./src/index.ts",
7
12
  "types": "./src/index.ts",
8
13
  "exports": {
@@ -94,6 +94,21 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
94
94
  "\n" +
95
95
  " if (body_owned) free(body_owned);\n" +
96
96
  "}\n",
97
+ "compiler/builtins/promise.ts":
98
+ "export const __Porffor_promise_resolve = (value: any, promise: any): void => {\n" +
99
+ " if (Porffor.type(value) == Porffor.TYPES.object) {\n" +
100
+ " const thenHash: i32 = __Porffor_object_hash('then');\n" +
101
+ " let probe: any = value;\n" +
102
+ " while (Porffor.type(probe) == Porffor.TYPES.object) {\n" +
103
+ " if (Porffor.object.lookup(probe, 'then', thenHash) != 0) break;\n" +
104
+ " probe = __Porffor_object_getPrototype(probe);\n" +
105
+ " }\n" +
106
+ " if (Porffor.type(probe) != Porffor.TYPES.object) {\n" +
107
+ " __ecma262_FulfillPromise(promise, value);\n" +
108
+ " return;\n" +
109
+ " }\n" +
110
+ " }\n" +
111
+ "};\n",
97
112
  };
98
113
  for (const [file, contents] of Object.entries(files)) {
99
114
  const path = join(source, file);
package/src/acquire.ts CHANGED
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
5
5
  import { resolve } from "node:path";
6
6
  // @ts-expect-error Bun's file loader supplies a path; see the pinned-commit
7
7
  // check below for why this is safe to trust unconditionally.
8
- import vendoredPorfforArchive from "../vendor/porffor-1f4ae4a.tar.gz" with { type: "file" };
8
+ import vendoredPorfforArchive from "../vendor/porffor-038f415.tar.gz" with { type: "file" };
9
9
  import { ensurePorfforPatched } from "./patch";
10
10
  import { PORFFOR_ARCHIVE_SHA256, PORFFOR_ARCHIVE_URL, PORFFOR_COMMIT_FULL } from "./pin";
11
11
 
@@ -26,7 +26,12 @@ type AcquireOptions = {
26
26
  timeoutMs?: number;
27
27
  };
28
28
 
29
- const required = ["runtime/index.js", "compiler/render.js", "compiler/index.js", "compiler/uwebsockets.js"];
29
+ const required = [
30
+ "runtime/index.js",
31
+ "compiler/render.js",
32
+ "compiler/index.js",
33
+ "compiler/uwebsockets.js",
34
+ ];
30
35
  async function digest(path: string): Promise<string> {
31
36
  return createHash("sha256")
32
37
  .update(await readFile(path))
@@ -42,9 +47,11 @@ async function complete(dir: string, expectedArchive = PORFFOR_ARCHIVE_SHA256):
42
47
  archiveSha256?: string;
43
48
  files?: Record<string, string>;
44
49
  };
45
- if (manifest.commit !== PORFFOR_COMMIT_FULL || manifest.archiveSha256 !== expectedArchive) return false;
50
+ if (manifest.commit !== PORFFOR_COMMIT_FULL || manifest.archiveSha256 !== expectedArchive)
51
+ return false;
46
52
  for (const file of required)
47
- if (!manifest.files?.[file] || (await digest(resolve(dir, file))) !== manifest.files[file]) return false;
53
+ if (!manifest.files?.[file] || (await digest(resolve(dir, file))) !== manifest.files[file])
54
+ return false;
48
55
  return true;
49
56
  } catch {
50
57
  return false;
@@ -81,10 +88,16 @@ async function extract(archive: string, stage: string): Promise<void> {
81
88
  });
82
89
  const [code, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
83
90
  if (code !== 0)
84
- throw new PorfforToolchainError("archive", `could not extract pinned Porffor archive: ${stderr.trim()}`);
91
+ throw new PorfforToolchainError(
92
+ "archive",
93
+ `could not extract pinned Porffor archive: ${stderr.trim()}`,
94
+ );
85
95
  for (const file of required)
86
96
  if (!existsSync(resolve(stage, file)))
87
- throw new PorfforToolchainError("archive", `Porffor archive is missing required file ${file}`);
97
+ throw new PorfforToolchainError(
98
+ "archive",
99
+ `Porffor archive is missing required file ${file}`,
100
+ );
88
101
  }
89
102
 
90
103
  async function waitForPublisher(dir: string, lock: string): Promise<string | null> {
@@ -113,7 +126,9 @@ export async function ensurePorffor(options: AcquireOptions = {}): Promise<strin
113
126
  return dir;
114
127
  }
115
128
  const root = resolve(
116
- options.cacheRoot ?? process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? resolve(homedir(), ".cache/sproutboat"),
129
+ options.cacheRoot ??
130
+ process.env.SPROUTBOAT_TOOLCHAIN_CACHE ??
131
+ resolve(homedir(), ".cache/sproutboat"),
117
132
  );
118
133
  const dir = resolve(root, `porffor-${PORFFOR_COMMIT_FULL}`);
119
134
  const expected = options.expectedSha256 ?? PORFFOR_ARCHIVE_SHA256;
@@ -127,7 +142,10 @@ export async function ensurePorffor(options: AcquireOptions = {}): Promise<strin
127
142
  if (published) return published;
128
143
  return ensurePorffor(options);
129
144
  }
130
- const stage = resolve(root, `.porffor-${PORFFOR_COMMIT_FULL}-${process.pid}-${crypto.randomUUID()}`);
145
+ const stage = resolve(
146
+ root,
147
+ `.porffor-${PORFFOR_COMMIT_FULL}-${process.pid}-${crypto.randomUUID()}`,
148
+ );
131
149
  try {
132
150
  if (await complete(dir, expected)) return dir;
133
151
  await rm(dir, { recursive: true, force: true });
@@ -139,9 +157,18 @@ export async function ensurePorffor(options: AcquireOptions = {}): Promise<strin
139
157
  // left stale by a pin bump (checksum below still catches a wrong file
140
158
  // rather than silently accepting it).
141
159
  const noOverride = options.url === undefined && options.expectedSha256 === undefined;
142
- const vendored = noOverride && existsSync(vendoredPorfforArchive) && (await digest(vendoredPorfforArchive)) === expected;
160
+ const vendored =
161
+ noOverride &&
162
+ existsSync(vendoredPorfforArchive) &&
163
+ (await digest(vendoredPorfforArchive)) === expected;
143
164
  if (vendored) await writeFile(archive, await readFile(vendoredPorfforArchive));
144
- else await download(options.url ?? PORFFOR_ARCHIVE_URL, archive, options.fetcher ?? fetch, options.timeoutMs ?? 30_000);
165
+ else
166
+ await download(
167
+ options.url ?? PORFFOR_ARCHIVE_URL,
168
+ archive,
169
+ options.fetcher ?? fetch,
170
+ options.timeoutMs ?? 30_000,
171
+ );
145
172
  const actual = await digest(archive);
146
173
  if (actual !== expected)
147
174
  throw new PorfforToolchainError(
@@ -152,7 +179,9 @@ export async function ensurePorffor(options: AcquireOptions = {}): Promise<strin
152
179
  await rm(archive, { force: true });
153
180
  await ensurePorfforPatched(stage);
154
181
  const files = Object.fromEntries(
155
- await Promise.all(required.map(async (file) => [file, await digest(resolve(stage, file))] as const)),
182
+ await Promise.all(
183
+ required.map(async (file) => [file, await digest(resolve(stage, file))] as const),
184
+ ),
156
185
  );
157
186
  await writeFile(
158
187
  resolve(stage, ".sproutboat-complete"),
package/src/patch.test.ts CHANGED
@@ -162,7 +162,9 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
162
162
 
163
163
  // #163: collect_headers takes res, drops a client-sent x-sb-remote-addr, and
164
164
  // appends the real peer; the call site passes res through.
165
- expect(once).toContain("static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {");
165
+ expect(once).toContain(
166
+ "static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {",
167
+ );
166
168
  expect(once).toContain("const i32 headers_ptr = collect_headers(req, res);");
167
169
  expect(once).toContain('if (key == "x-sb-remote-addr") continue;');
168
170
  expect(once).toContain("res->getRemoteAddressAsText()");
@@ -179,12 +181,22 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
179
181
  );
180
182
  expect(once).toContain('key == "x-sb-raw-body"');
181
183
  expect(once).toContain("bool raw_body = false;");
182
- expect(once).toContain('if (std::string_view(scan_buf, scan_bytes) == "x-sb-raw-body") raw_body = true;');
183
- expect(once).toContain("if (raw_body) porf_native_fetch_read_raw_bytes(body_value, &body_buf, &body_len);");
184
- expect(once).toContain("else porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);");
184
+ expect(once).toContain(
185
+ 'if (std::string_view(scan_buf, scan_bytes) == "x-sb-raw-body") raw_body = true;',
186
+ );
187
+ expect(once).toContain(
188
+ "if (raw_body) porf_native_fetch_read_raw_bytes(body_value, &body_buf, &body_len);",
189
+ );
190
+ expect(once).toContain(
191
+ "else porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);",
192
+ );
185
193
  // Header reads are the original 4-arg call, unchanged.
186
- expect(once).toContain("porf_native_fetch_read_value(name_value, &name_buf, &name_len, &name_owned);");
187
- expect(once).toContain("porf_native_fetch_read_value(value_value, &value_buf, &value_len, &value_owned);");
194
+ expect(once).toContain(
195
+ "porf_native_fetch_read_value(name_value, &name_buf, &name_len, &name_owned);",
196
+ );
197
+ expect(once).toContain(
198
+ "porf_native_fetch_read_value(value_value, &value_buf, &value_len, &value_owned);",
199
+ );
188
200
 
189
201
  await patchUwebsockets(root);
190
202
  expect(await readFile(join(root, "compiler/uwebsockets.js"), "utf8")).toBe(once);
package/src/patch.ts CHANGED
@@ -108,7 +108,8 @@ const BYTESTRING_MARKER = "sproutboat #172";
108
108
  * below) calls it, gated on the reserved `x-sb-raw-body` response header the
109
109
  * assets binding sets. Every existing call site is untouched.
110
110
  */
111
- const READ_RAW_ANCHOR = "int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned) {";
111
+ const READ_RAW_ANCHOR =
112
+ "int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned) {";
112
113
  const READ_RAW_INJECT =
113
114
  "// sproutboat #176: zero-copy passthrough for a bytestring that is already-\n" +
114
115
  "// finished bytes (an asset read off disk), never a string to UTF-8 encode.\n" +
@@ -208,7 +209,8 @@ const STATUS_SIG_MARKER = "static std::string lookup_status_line(i32 status) {";
208
209
  // 303 is the one this was reported for (POST-redirect-GET); give it the real
209
210
  // reason phrase. Everything else unlisted rides the synthesized fallback below.
210
211
  const STATUS_303_ANCHOR = ' case 302: return "302 Found";\n';
211
- const STATUS_303_INJECT = ' case 302: return "302 Found";\n case 303: return "303 See Other";\n';
212
+ const STATUS_303_INJECT =
213
+ ' case 302: return "302 Found";\n case 303: return "303 See Other";\n';
212
214
  const STATUS_303_MARKER = 'case 303: return "303 See Other";';
213
215
  const STATUS_DEFAULT_ANCHOR = " default: return {};";
214
216
  const STATUS_DEFAULT_INJECT = ' default: return std::to_string(status) + " Status";';
@@ -225,7 +227,8 @@ const STATUS_DEFAULT_MARKER = 'std::to_string(status) + " Status"';
225
227
  * `request.cf.clientIp` and resolves it against `SB_TRUSTED_PROXIES` if set.
226
228
  */
227
229
  const HDR_SIG_ANCHOR = "static i32 collect_headers(uWS::HttpRequest* req) {\n";
228
- const HDR_SIG_INJECT = "static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {\n";
230
+ const HDR_SIG_INJECT =
231
+ "static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {\n";
229
232
  const HDR_SIG_MARKER = "collect_headers(uWS::HttpRequest* req, uWS::HttpResponse";
230
233
 
231
234
  const HDR_CALL_ANCHOR = "const i32 headers_ptr = collect_headers(req);";
@@ -361,7 +364,7 @@ const WRITE_RESPONSE_INJECT =
361
364
  " size_t scan_bytes = 0;\n" +
362
365
  " char* scan_owned = nullptr;\n" +
363
366
  " porf_native_fetch_read_value(scan_name, &scan_buf, &scan_bytes, &scan_owned);\n" +
364
- " if (std::string_view(scan_buf, scan_bytes) == \"x-sb-raw-body\") raw_body = true;\n" +
367
+ ' if (std::string_view(scan_buf, scan_bytes) == "x-sb-raw-body") raw_body = true;\n' +
365
368
  " if (scan_owned) free(scan_owned);\n" +
366
369
  " }\n" +
367
370
  " }\n" +
@@ -417,15 +420,30 @@ const UWS_EDITS = [
417
420
  [BODY_MARKER, BODY_ANCHOR, BODY_INJECT, "request body limit"],
418
421
  [STATUS_SIG_MARKER, STATUS_SIG_ANCHOR, STATUS_SIG_INJECT, "status-line return type (#156)"],
419
422
  [STATUS_303_MARKER, STATUS_303_ANCHOR, STATUS_303_INJECT, "status-line 303 case (#156)"],
420
- [STATUS_DEFAULT_MARKER, STATUS_DEFAULT_ANCHOR, STATUS_DEFAULT_INJECT, "status-line fallback (#156)"],
423
+ [
424
+ STATUS_DEFAULT_MARKER,
425
+ STATUS_DEFAULT_ANCHOR,
426
+ STATUS_DEFAULT_INJECT,
427
+ "status-line fallback (#156)",
428
+ ],
421
429
  [HDR_SIG_MARKER, HDR_SIG_ANCHOR, HDR_SIG_INJECT, "collect_headers res arg (#163)"],
422
430
  [HDR_CALL_MARKER, HDR_CALL_ANCHOR, HDR_CALL_INJECT, "collect_headers call site (#163)"],
423
431
  [HDR_CAP_MARKER, HDR_CAP_ANCHOR, HDR_CAP_INJECT, "remote-addr header capacity (#163)"],
424
432
  [HDR_SKIP_MARKER, HDR_SKIP_ANCHOR, HDR_SKIP_INJECT, "drop client-sent x-sb-remote-addr (#163)"],
425
433
  [HDR_APPEND_MARKER, HDR_APPEND_ANCHOR, HDR_APPEND_INJECT, "append x-sb-remote-addr (#163)"],
426
434
  [READ_RAW_DECL_MARKER, READ_RAW_DECL_ANCHOR, READ_RAW_DECL_INJECT, "read_raw_bytes decl (#176)"],
427
- [FORBIDDEN_HDR_MARKER, FORBIDDEN_HDR_ANCHOR, FORBIDDEN_HDR_INJECT, "drop x-sb-raw-body from the wire (#176)"],
428
- [WRITE_RESPONSE_MARKER, WRITE_RESPONSE_ANCHOR, WRITE_RESPONSE_INJECT, "detect x-sb-raw-body, steer the body read (#176)"],
435
+ [
436
+ FORBIDDEN_HDR_MARKER,
437
+ FORBIDDEN_HDR_ANCHOR,
438
+ FORBIDDEN_HDR_INJECT,
439
+ "drop x-sb-raw-body from the wire (#176)",
440
+ ],
441
+ [
442
+ WRITE_RESPONSE_MARKER,
443
+ WRITE_RESPONSE_ANCHOR,
444
+ WRITE_RESPONSE_INJECT,
445
+ "detect x-sb-raw-body, steer the body read (#176)",
446
+ ],
429
447
  ] as const;
430
448
 
431
449
  /** The uWebSockets shim source: body limit + the #156 status-line fix. Exported for tests. */
package/src/pin.ts CHANGED
@@ -10,13 +10,14 @@
10
10
  * uWebSockets archive in `sproutboat-cli/vendor/` — then run both repos'
11
11
  * test + conformance suites. See sproutboat-cli/MIGRATION.md.
12
12
  */
13
- export const PORFFOR_CHANNEL = "alpha-5";
14
- export const PORFFOR_COMMIT_FULL = "1f4ae4ae3e0a5f0a93b3bc084359e1a3a23391fd";
13
+ export const PORFFOR_CHANNEL = "alpha-6";
14
+ export const PORFFOR_COMMIT_FULL = "038f415e08efc5f87a6bfcb05a18824caa3a14f6";
15
15
  export const PORFFOR_COMMIT = PORFFOR_COMMIT_FULL.slice(0, 7);
16
- export const PORFFOR_ARCHIVE_SHA256 = "a49a0e857574e93cb09c574dcf53b7cc049d3cb65c5b944b169755cad2a51d5d";
16
+ export const PORFFOR_ARCHIVE_SHA256 =
17
+ "61f65d8fac5f94a7910f0a3f046e8929a852addb0033a032b9f7e08d0b07df5c";
17
18
  export const PORFFOR_ARCHIVE_URL = `https://codeload.github.com/CanadaHonk/porffor/tar.gz/${PORFFOR_COMMIT_FULL}`;
18
19
 
19
- /** A compact identity string for a manifest / report (`alpha-5 (1f4ae4a)`). */
20
+ /** A compact identity string for a manifest / report (`alpha-6 (038f415)`). */
20
21
  export function porfforVersion(): string {
21
22
  return process.env.PORFFOR_VERSION || `${PORFFOR_CHANNEL} (${PORFFOR_COMMIT})`;
22
23
  }
Binary file
Binary file