@aresdefencelabs/wasm-http-runtime 0.0.1 → 0.0.3

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/dist/abi.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import type { RuntimeState } from "./types";
2
2
  export declare function createAresAbiImports<Env>(state: RuntimeState<Env>): {
3
- ares_abi: {
3
+ env: {
4
4
  abi_log(messagePtr: number): void;
5
5
  abi_http_get_user_agent_name(): number;
6
+ abi_http_fetch_blocking(requestJsonCstrPtr: number): Promise<number>;
7
+ abi_http_response_get_status(responseId: number): number;
8
+ abi_http_response_get_body_len(responseId: number): number;
9
+ abi_http_response_copy_body(responseId: number, outPtr: number, maxLen: number): number;
10
+ abi_http_response_copy_header(responseId: number, keyCstrPtr: number, outPtr: number, maxLen: number): number;
11
+ abi_http_response_free(responseId: number): void;
6
12
  };
7
13
  };
package/dist/abi.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readCString, writeCString } from "./memory";
1
+ import { encodeUtf8, readCString, utf8ByteLength, writeBytesIntoMemory, writeCString } from "./memory";
2
2
  function getInstanceOrThrow(state) {
3
3
  if (!state.instance) {
4
4
  throw new Error("Wasm instance has not been initialised yet.");
@@ -11,9 +11,60 @@ function getRequestContextOrThrow(state) {
11
11
  }
12
12
  return state.requestContext;
13
13
  }
14
+ function normalizeHeaders(headers) {
15
+ const out = {};
16
+ for (const [key, value] of headers.entries()) {
17
+ out[key.toLowerCase()] = value;
18
+ }
19
+ return out;
20
+ }
21
+ function estimateResponseBytes(resp) {
22
+ let total = utf8ByteLength(resp.bodyText);
23
+ for (const [k, v] of Object.entries(resp.headers)) {
24
+ total += utf8ByteLength(k);
25
+ total += utf8ByteLength(v);
26
+ }
27
+ return total >>> 0;
28
+ }
29
+ function sweepExpiredResponses(state) {
30
+ const now = Date.now();
31
+ const ttl = state.options.responseTtlMs;
32
+ for (const [id, entry] of state.httpResponses.entries()) {
33
+ if (now - entry.createdAtMs > ttl) {
34
+ state.httpResponses.delete(id);
35
+ }
36
+ }
37
+ }
38
+ function getCurrentBridgeBytes(state) {
39
+ let total = 0;
40
+ for (const entry of state.httpResponses.values()) {
41
+ total += entry.estimatedBytes;
42
+ }
43
+ return total >>> 0;
44
+ }
45
+ function assertBridgeCapacity(state, nextEntryBytes) {
46
+ sweepExpiredResponses(state);
47
+ if (state.httpResponses.size >= state.options.maxHttpResponseHandles) {
48
+ throw new Error("Too many outstanding HTTP response handles");
49
+ }
50
+ const totalAfterInsert = getCurrentBridgeBytes(state) + nextEntryBytes;
51
+ if (totalAfterInsert > state.options.maxTotalBridgeBytes) {
52
+ throw new Error("HTTP response bridge memory budget exceeded");
53
+ }
54
+ }
55
+ function getResponseByIdOrEmpty(state, responseId) {
56
+ sweepExpiredResponses(state);
57
+ return (state.httpResponses.get(responseId) ?? {
58
+ status: 0,
59
+ bodyText: "",
60
+ headers: {},
61
+ createdAtMs: 0,
62
+ estimatedBytes: 0
63
+ });
64
+ }
14
65
  export function createAresAbiImports(state) {
15
66
  return {
16
- ares_abi: {
67
+ env: {
17
68
  abi_log(messagePtr) {
18
69
  const instance = getInstanceOrThrow(state);
19
70
  const message = readCString(instance.exports.memory, messagePtr);
@@ -29,6 +80,56 @@ export function createAresAbiImports(state) {
29
80
  const ctx = getRequestContextOrThrow(state);
30
81
  const userAgent = ctx.request.headers.get("user-agent") ?? "Cloudflare-Worker";
31
82
  return writeCString(instance.exports.memory, instance.exports.alloc, userAgent);
83
+ },
84
+ async abi_http_fetch_blocking(requestJsonCstrPtr) {
85
+ const instance = getInstanceOrThrow(state);
86
+ const requestJson = readCString(instance.exports.memory, requestJsonCstrPtr);
87
+ const outbound = JSON.parse(requestJson);
88
+ const response = await fetch(outbound.url, {
89
+ method: outbound.method ?? "GET",
90
+ headers: outbound.headers,
91
+ body: outbound.body ?? undefined
92
+ });
93
+ const bodyText = await response.text();
94
+ const bodyBytes = utf8ByteLength(bodyText);
95
+ if (bodyBytes > state.options.maxResponseBodyBytes) {
96
+ throw new Error(`HTTP response body too large: ${bodyBytes} bytes exceeds maxResponseBodyBytes=${state.options.maxResponseBodyBytes}`);
97
+ }
98
+ const headers = normalizeHeaders(response.headers);
99
+ const estimatedBytes = estimateResponseBytes({
100
+ bodyText,
101
+ headers
102
+ });
103
+ assertBridgeCapacity(state, estimatedBytes);
104
+ const responseId = state.nextHttpResponseId++;
105
+ state.httpResponses.set(responseId, {
106
+ status: response.status,
107
+ bodyText,
108
+ headers,
109
+ createdAtMs: Date.now(),
110
+ estimatedBytes
111
+ });
112
+ return responseId >>> 0;
113
+ },
114
+ abi_http_response_get_status(responseId) {
115
+ return getResponseByIdOrEmpty(state, responseId).status >>> 0;
116
+ },
117
+ abi_http_response_get_body_len(responseId) {
118
+ return utf8ByteLength(getResponseByIdOrEmpty(state, responseId).bodyText);
119
+ },
120
+ abi_http_response_copy_body(responseId, outPtr, maxLen) {
121
+ const instance = getInstanceOrThrow(state);
122
+ const bodyText = getResponseByIdOrEmpty(state, responseId).bodyText;
123
+ return writeBytesIntoMemory(instance.exports.memory, outPtr, encodeUtf8(bodyText), maxLen);
124
+ },
125
+ abi_http_response_copy_header(responseId, keyCstrPtr, outPtr, maxLen) {
126
+ const instance = getInstanceOrThrow(state);
127
+ const key = readCString(instance.exports.memory, keyCstrPtr).toLowerCase();
128
+ const value = getResponseByIdOrEmpty(state, responseId).headers[key] ?? "";
129
+ return writeBytesIntoMemory(instance.exports.memory, outPtr, encodeUtf8(value), maxLen);
130
+ },
131
+ abi_http_response_free(responseId) {
132
+ state.httpResponses.delete(responseId);
32
133
  }
33
134
  }
34
135
  };
package/dist/memory.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- export declare function readCString(memory: WebAssembly.Memory, ptr: number): string;
1
+ export declare function encodeUtf8(value: string): Uint8Array;
2
+ export declare function utf8ByteLength(value: string): number;
3
+ export declare function readCString(memory: WebAssembly.Memory, ptr: number, maxLen?: number): string;
2
4
  export declare function writeCString(memory: WebAssembly.Memory, alloc: (size: number) => number, value: string): number;
3
5
  export declare function writeJsonCString(memory: WebAssembly.Memory, alloc: (size: number) => number, value: unknown): number;
6
+ export declare function writeBytesIntoMemory(memory: WebAssembly.Memory, outPtr: number, src: Uint8Array, maxLen: number): number;
4
7
  export declare function headersToObject(headers: Headers): Record<string, string>;
package/dist/memory.js CHANGED
@@ -1,24 +1,58 @@
1
1
  const encoder = new TextEncoder();
2
2
  const decoder = new TextDecoder();
3
- export function readCString(memory, ptr) {
3
+ export function encodeUtf8(value) {
4
+ return encoder.encode(value);
5
+ }
6
+ export function utf8ByteLength(value) {
7
+ return encoder.encode(value).length >>> 0;
8
+ }
9
+ export function readCString(memory, ptr, maxLen = 10_000_000) {
10
+ if (!ptr)
11
+ return "";
4
12
  const bytes = new Uint8Array(memory.buffer);
5
- let end = ptr;
6
- while (bytes[end] !== 0) {
13
+ const start = ptr >>> 0;
14
+ if (start >= bytes.length) {
15
+ throw new Error(`CString pointer out of bounds: ${ptr}`);
16
+ }
17
+ let end = start;
18
+ const limit = Math.min(bytes.length, start + maxLen);
19
+ while (end < limit && bytes[end] !== 0) {
7
20
  end++;
8
21
  }
9
- return decoder.decode(bytes.subarray(ptr, end));
22
+ if (end >= limit) {
23
+ throw new Error("CString terminator not found within safe bounds");
24
+ }
25
+ return decoder.decode(bytes.subarray(start, end));
10
26
  }
11
27
  export function writeCString(memory, alloc, value) {
12
28
  const encoded = encoder.encode(value);
13
29
  const ptr = alloc(encoded.length + 1);
30
+ if (!ptr) {
31
+ throw new Error(`alloc failed for ${encoded.length + 1} bytes`);
32
+ }
14
33
  const bytes = new Uint8Array(memory.buffer);
15
34
  bytes.set(encoded, ptr);
16
35
  bytes[ptr + encoded.length] = 0;
17
- return ptr;
36
+ return ptr >>> 0;
18
37
  }
19
38
  export function writeJsonCString(memory, alloc, value) {
20
39
  return writeCString(memory, alloc, JSON.stringify(value));
21
40
  }
41
+ export function writeBytesIntoMemory(memory, outPtr, src, maxLen) {
42
+ if (!outPtr || maxLen <= 0)
43
+ return 0;
44
+ const bytes = new Uint8Array(memory.buffer);
45
+ const start = outPtr >>> 0;
46
+ if (start >= bytes.length) {
47
+ return 0;
48
+ }
49
+ const writeLen = Math.min(src.length, maxLen, bytes.length - start);
50
+ if (writeLen <= 0) {
51
+ return 0;
52
+ }
53
+ bytes.set(src.subarray(0, writeLen), start);
54
+ return writeLen >>> 0;
55
+ }
22
56
  export function headersToObject(headers) {
23
57
  return Object.fromEntries(headers.entries());
24
58
  }
package/dist/runtime.d.ts CHANGED
@@ -6,5 +6,7 @@ export declare class AresWorkerRuntime<Env = unknown> {
6
6
  setRequestContext(context: RequestScopedContext<Env>): void;
7
7
  clearRequestContext(): void;
8
8
  get instance(): WasmInstanceWithExports;
9
+ private freeIfNeeded;
10
+ private jsonErrorResponse;
9
11
  handleFetch(request: Request, env: Env, ctx: WorkerExecutionContext): Promise<Response>;
10
12
  }
package/dist/runtime.js CHANGED
@@ -1,24 +1,64 @@
1
1
  import { createAresAbiImports } from "./abi";
2
2
  import { headersToObject, readCString, writeJsonCString } from "./memory";
3
+ const DEFAULT_MAX_HTTP_RESPONSE_HANDLES = 32;
4
+ const DEFAULT_MAX_TOTAL_BRIDGE_BYTES = 8 * 1024 * 1024; // 8 MB total pool
5
+ const DEFAULT_MAX_RESPONSE_BODY_BYTES = 512 * 1024; // 512 KB per response
6
+ const DEFAULT_RESPONSE_TTL_MS = 30_000;
7
+ const NOOP_LOGGER = (_) => { };
8
+ function withDefaults(options) {
9
+ return {
10
+ wasm: options.wasm,
11
+ debug: options.debug ?? false,
12
+ onLog: options.onLog ?? NOOP_LOGGER,
13
+ env: options.env,
14
+ maxHttpResponseHandles: options.maxHttpResponseHandles ?? DEFAULT_MAX_HTTP_RESPONSE_HANDLES,
15
+ maxTotalBridgeBytes: options.maxTotalBridgeBytes ?? DEFAULT_MAX_TOTAL_BRIDGE_BYTES,
16
+ maxResponseBodyBytes: options.maxResponseBodyBytes ?? DEFAULT_MAX_RESPONSE_BODY_BYTES,
17
+ responseTtlMs: options.responseTtlMs ?? DEFAULT_RESPONSE_TTL_MS
18
+ };
19
+ }
3
20
  export class AresWorkerRuntime {
4
21
  state;
5
22
  constructor(options) {
6
23
  this.state = {
7
24
  instance: null,
8
- options,
9
- requestContext: null
25
+ options: withDefaults(options),
26
+ requestContext: null,
27
+ initPromise: null,
28
+ httpResponses: new Map(),
29
+ nextHttpResponseId: 1
10
30
  };
11
31
  }
12
32
  async init() {
13
33
  if (this.state.instance) {
14
34
  return;
15
35
  }
16
- const imports = createAresAbiImports(this.state);
17
- const instance = await WebAssembly.instantiate(this.state.options.wasm, imports);
18
- this.state.instance = instance;
19
- if (instance.exports.app_init) {
20
- instance.exports.app_init();
36
+ if (this.state.initPromise) {
37
+ return this.state.initPromise;
21
38
  }
39
+ this.state.initPromise = (async () => {
40
+ const imports = createAresAbiImports(this.state);
41
+ const instantiated = await WebAssembly.instantiate(this.state.options.wasm, imports);
42
+ const instance = instantiated &&
43
+ typeof instantiated === "object" &&
44
+ "instance" in instantiated
45
+ ? instantiated.instance
46
+ : instantiated;
47
+ this.state.instance = instance;
48
+ if (!(instance.exports.memory instanceof WebAssembly.Memory)) {
49
+ throw new Error("Wasm export memory was not found.");
50
+ }
51
+ if (typeof instance.exports.alloc !== "function") {
52
+ throw new Error("Wasm export alloc() was not found.");
53
+ }
54
+ if (typeof instance.exports.app_init === "function") {
55
+ const rc = await Promise.resolve(instance.exports.app_init());
56
+ if (rc !== 0) {
57
+ throw new Error(`Wasm app_init() failed with rc=${rc}`);
58
+ }
59
+ }
60
+ })();
61
+ return this.state.initPromise;
22
62
  }
23
63
  setRequestContext(context) {
24
64
  this.state.requestContext = context;
@@ -32,12 +72,32 @@ export class AresWorkerRuntime {
32
72
  }
33
73
  return this.state.instance;
34
74
  }
75
+ freeIfNeeded(ptr, size = 0) {
76
+ if (!ptr)
77
+ return;
78
+ if (typeof this.instance.exports.free_mem === "function") {
79
+ this.instance.exports.free_mem(ptr, size);
80
+ }
81
+ }
82
+ jsonErrorResponse(message, status = 500) {
83
+ return new Response(JSON.stringify({
84
+ error: "worker_wasm_failure",
85
+ message
86
+ }), {
87
+ status,
88
+ headers: {
89
+ "content-type": "application/json; charset=utf-8"
90
+ }
91
+ });
92
+ }
35
93
  async handleFetch(request, env, ctx) {
94
+ let requestPtr = 0;
95
+ let responsePtr = 0;
36
96
  await this.init();
37
97
  this.setRequestContext({ request, env, ctx });
38
98
  try {
39
- if (!this.instance.exports.handle_http) {
40
- throw new Error("Wasm export handle_http() was not found.");
99
+ if (typeof this.instance.exports.handle_http_json !== "function") {
100
+ throw new Error("Wasm export handle_http_json() was not found.");
41
101
  }
42
102
  const requestBody = await request.text();
43
103
  const inboundEnvelope = {
@@ -46,18 +106,30 @@ export class AresWorkerRuntime {
46
106
  headers: headersToObject(request.headers),
47
107
  body: requestBody
48
108
  };
49
- const requestPtr = writeJsonCString(this.instance.exports.memory, this.instance.exports.alloc, inboundEnvelope);
50
- const responsePtr = this.instance.exports.handle_http(requestPtr);
109
+ requestPtr = writeJsonCString(this.instance.exports.memory, this.instance.exports.alloc, inboundEnvelope);
110
+ const responsePtrOrPromise = this.instance.exports.handle_http_json(requestPtr);
111
+ responsePtr = await Promise.resolve(responsePtrOrPromise);
112
+ if (!responsePtr) {
113
+ throw new Error("Wasm returned a null response pointer.");
114
+ }
51
115
  const responseJson = readCString(this.instance.exports.memory, responsePtr);
52
116
  const responseEnvelope = JSON.parse(responseJson);
53
117
  return new Response(typeof responseEnvelope.body === "string"
54
118
  ? responseEnvelope.body
55
119
  : JSON.stringify(responseEnvelope.body), {
56
- status: responseEnvelope.status,
57
- headers: responseEnvelope.headers
120
+ status: responseEnvelope.status || 200,
121
+ headers: responseEnvelope.headers || {
122
+ "content-type": "application/json; charset=utf-8"
123
+ }
58
124
  });
59
125
  }
126
+ catch (error) {
127
+ const message = error instanceof Error ? error.message : String(error);
128
+ return this.jsonErrorResponse(message);
129
+ }
60
130
  finally {
131
+ this.freeIfNeeded(requestPtr);
132
+ this.freeIfNeeded(responsePtr);
61
133
  this.clearRequestContext();
62
134
  }
63
135
  }
package/dist/types.d.ts CHANGED
@@ -6,8 +6,8 @@ export type WasmExports = {
6
6
  memory: WebAssembly.Memory;
7
7
  alloc: (size: number) => number;
8
8
  free_mem?: (ptr: number, size: number) => void;
9
- app_init?: () => number;
10
- handle_http?: (requestJsonPtr: number) => number;
9
+ app_init?: () => number | Promise<number>;
10
+ handle_http_json?: (requestJsonPtr: number) => number | Promise<number>;
11
11
  };
12
12
  export type WasmInstanceWithExports = WebAssembly.Instance & {
13
13
  exports: WasmExports;
@@ -23,11 +23,28 @@ export type ResponseEnvelope = {
23
23
  headers: Record<string, string>;
24
24
  body: unknown;
25
25
  };
26
+ export type OutboundHttpRequestEnvelope = {
27
+ url: string;
28
+ method?: string;
29
+ headers?: Record<string, string>;
30
+ body?: string | null;
31
+ };
32
+ export type HttpBridgeResponseCache = {
33
+ status: number;
34
+ bodyText: string;
35
+ headers: Record<string, string>;
36
+ createdAtMs: number;
37
+ estimatedBytes: number;
38
+ };
26
39
  export type RuntimeOptions<Env = unknown> = {
27
40
  wasm: WebAssembly.Module;
28
41
  debug?: boolean;
29
42
  onLog?: (message: string) => void;
30
43
  env?: Env;
44
+ maxHttpResponseHandles?: number;
45
+ maxTotalBridgeBytes?: number;
46
+ maxResponseBodyBytes?: number;
47
+ responseTtlMs?: number;
31
48
  };
32
49
  export type RequestScopedContext<Env = unknown> = {
33
50
  request: Request;
@@ -36,6 +53,9 @@ export type RequestScopedContext<Env = unknown> = {
36
53
  };
37
54
  export type RuntimeState<Env = unknown> = {
38
55
  instance: WasmInstanceWithExports | null;
39
- options: RuntimeOptions<Env>;
56
+ options: Required<Pick<RuntimeOptions<Env>, "wasm" | "debug" | "onLog" | "env" | "maxHttpResponseHandles" | "maxTotalBridgeBytes" | "maxResponseBodyBytes" | "responseTtlMs">>;
40
57
  requestContext: RequestScopedContext<Env> | null;
58
+ initPromise: Promise<void> | null;
59
+ httpResponses: Map<number, HttpBridgeResponseCache>;
60
+ nextHttpResponseId: number;
41
61
  };
package/dist/worker.js CHANGED
@@ -4,8 +4,10 @@ export function createAresWorkerHandler(wasm, options) {
4
4
  wasm,
5
5
  ...options
6
6
  });
7
+ const ready = runtime.init();
7
8
  return {
8
9
  async fetch(request, env, ctx) {
10
+ await ready;
9
11
  return runtime.handleFetch(request, env, ctx);
10
12
  }
11
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aresdefencelabs/wasm-http-runtime",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Runtime adapter that connects C++ WebAssembly workers to the Cloudflare Workers runtime via an ABI bridge.",
5
5
  "type": "module",
6
6
  "private": false,