@ricsam/quickjs-fetch 0.2.2 → 0.2.4
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/cjs/{abort-controller.cjs.map → globals/abort-controller.cjs.map} +1 -1
- package/dist/cjs/{fetch.cjs.map → globals/fetch.cjs.map} +1 -1
- package/dist/cjs/{form-data.cjs.map → globals/form-data.cjs.map} +1 -1
- package/dist/cjs/{headers.cjs.map → globals/headers.cjs.map} +1 -1
- package/dist/cjs/{request.cjs.map → globals/request.cjs.map} +1 -1
- package/dist/cjs/{response.cjs.map → globals/response.cjs.map} +1 -1
- package/dist/cjs/{serve.cjs.map → globals/serve.cjs.map} +1 -1
- package/dist/cjs/handle.cjs +12 -2
- package/dist/cjs/handle.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/{abort-controller.mjs.map → globals/abort-controller.mjs.map} +1 -1
- package/dist/mjs/{fetch.mjs.map → globals/fetch.mjs.map} +1 -1
- package/dist/mjs/{form-data.mjs.map → globals/form-data.mjs.map} +1 -1
- package/dist/mjs/{headers.mjs.map → globals/headers.mjs.map} +1 -1
- package/dist/mjs/{request.mjs.map → globals/request.mjs.map} +1 -1
- package/dist/mjs/{response.mjs.map → globals/response.mjs.map} +1 -1
- package/dist/mjs/{serve.mjs.map → globals/serve.mjs.map} +1 -1
- package/dist/mjs/handle.mjs +12 -2
- package/dist/mjs/handle.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/package.json +2 -2
- /package/dist/cjs/{abort-controller.cjs → globals/abort-controller.cjs} +0 -0
- /package/dist/cjs/{fetch.cjs → globals/fetch.cjs} +0 -0
- /package/dist/cjs/{form-data.cjs → globals/form-data.cjs} +0 -0
- /package/dist/cjs/{headers.cjs → globals/headers.cjs} +0 -0
- /package/dist/cjs/{request.cjs → globals/request.cjs} +0 -0
- /package/dist/cjs/{response.cjs → globals/response.cjs} +0 -0
- /package/dist/cjs/{serve.cjs → globals/serve.cjs} +0 -0
- /package/dist/mjs/{abort-controller.mjs → globals/abort-controller.mjs} +0 -0
- /package/dist/mjs/{fetch.mjs → globals/fetch.mjs} +0 -0
- /package/dist/mjs/{form-data.mjs → globals/form-data.mjs} +0 -0
- /package/dist/mjs/{headers.mjs → globals/headers.mjs} +0 -0
- /package/dist/mjs/{request.mjs → globals/request.mjs} +0 -0
- /package/dist/mjs/{response.mjs → globals/response.mjs} +0 -0
- /package/dist/mjs/{serve.mjs → globals/serve.mjs} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/abort-controller.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext } from \"quickjs-emscripten\";\n\n/**\n * Setup AbortController and AbortSignal classes in QuickJS.\n * Uses pure QuickJS implementation so that:\n * - controller.signal returns an actual AbortSignal instance\n * - Event listeners work correctly\n * - All prototype methods are accessible\n */\nexport function setupAbortControllerAndSignal(context: QuickJSContext): void {\n // Track signals for timeout feature - we need to call back into QuickJS after a delay\n const pendingTimeouts = new Map<number, ReturnType<typeof setTimeout>>();\n let nextTimeoutId = 1;\n\n // Create a host function that schedules a timeout and returns an ID\n const scheduleTimeoutFn = context.newFunction(\"__scheduleTimeout__\", (msHandle) => {\n const ms = context.getNumber(msHandle);\n const timeoutId = nextTimeoutId++;\n\n // Schedule the timeout - we'll call back into QuickJS using a polling mechanism\n const hostTimeoutId = setTimeout(() => {\n pendingTimeouts.delete(timeoutId);\n }, ms);\n\n pendingTimeouts.set(timeoutId, hostTimeoutId);\n return context.newNumber(timeoutId);\n });\n\n // Create a host function to check if a timeout has elapsed\n const checkTimeoutFn = context.newFunction(\"__checkTimeout__\", (timeoutIdHandle) => {\n const timeoutId = context.getNumber(timeoutIdHandle);\n // If the timeout is no longer in pendingTimeouts, it has elapsed\n const isElapsed = !pendingTimeouts.has(timeoutId);\n return isElapsed ? context.true : context.false;\n });\n\n context.setProp(context.global, \"__scheduleTimeout__\", scheduleTimeoutFn);\n context.setProp(context.global, \"__checkTimeout__\", checkTimeoutFn);\n scheduleTimeoutFn.dispose();\n checkTimeoutFn.dispose();\n\n // Use WeakMap for private state (more compatible than private fields)\n // Note: AbortSignal.timeout() uses a polling approach since we can't easily\n // call back into QuickJS from a host setTimeout callback\n const result = context.evalCode(`\n (function() {\n const signalState = new WeakMap();\n\n class AbortSignal {\n constructor() {\n signalState.set(this, {\n aborted: false,\n reason: undefined,\n listeners: []\n });\n }\n\n get aborted() {\n return signalState.get(this).aborted;\n }\n\n get reason() {\n return signalState.get(this).reason;\n }\n\n throwIfAborted() {\n const state = signalState.get(this);\n if (state.aborted) {\n throw state.reason ?? new DOMException(\"signal is aborted\", \"AbortError\");\n }\n }\n\n addEventListener(type, callback) {\n if (type === \"abort\" && typeof callback === \"function\") {\n signalState.get(this).listeners.push(callback);\n }\n }\n\n removeEventListener(type, callback) {\n if (type === \"abort\") {\n const state = signalState.get(this);\n const idx = state.listeners.indexOf(callback);\n if (idx >= 0) state.listeners.splice(idx, 1);\n }\n }\n\n static abort(reason) {\n const signal = new AbortSignal();\n const state = signalState.get(signal);\n state.aborted = true;\n state.reason = reason ?? new DOMException(\"signal is aborted\", \"AbortError\");\n return signal;\n }\n\n static timeout(ms) {\n const signal = new AbortSignal();\n const state = signalState.get(signal);\n const timeoutId = __scheduleTimeout__(Number(ms));\n\n // Store timeout info for later checking\n state.timeoutId = timeoutId;\n state.timeoutReason = new DOMException(\"signal timed out\", \"TimeoutError\");\n\n return signal;\n }\n\n // Internal method to check and trigger timeout\n __checkAndTriggerTimeout__() {\n const state = signalState.get(this);\n if (state.aborted || state.timeoutId === undefined) {\n return false;\n }\n\n if (__checkTimeout__(state.timeoutId)) {\n state.aborted = true;\n state.reason = state.timeoutReason;\n delete state.timeoutId;\n delete state.timeoutReason;\n\n for (const cb of state.listeners) {\n try { cb(); } catch {}\n }\n return true;\n }\n return false;\n }\n\n static any(signals) {\n const signal = new AbortSignal();\n const state = signalState.get(signal);\n\n if (!Array.isArray(signals)) return signal;\n\n for (const s of signals) {\n if (s && typeof s === \"object\" && \"aborted\" in s) {\n // Check timeout signals\n if (s.__checkAndTriggerTimeout__) {\n s.__checkAndTriggerTimeout__();\n }\n\n if (s.aborted) {\n state.aborted = true;\n state.reason = s.reason;\n return signal;\n }\n s.addEventListener(\"abort\", () => {\n if (!state.aborted) {\n state.aborted = true;\n state.reason = s.reason;\n for (const cb of state.listeners) {\n try { cb(); } catch {}\n }\n }\n });\n }\n }\n return signal;\n }\n }\n\n const controllerState = new WeakMap();\n\n class AbortController {\n constructor() {\n const signal = new AbortSignal();\n controllerState.set(this, { signal });\n }\n\n get signal() {\n return controllerState.get(this).signal;\n }\n\n abort(reason) {\n const signal = controllerState.get(this).signal;\n const state = signalState.get(signal);\n if (state.aborted) return;\n\n state.aborted = true;\n state.reason = reason ?? new DOMException(\"signal is aborted\", \"AbortError\");\n\n for (const cb of state.listeners) {\n try { cb(); } catch {}\n }\n }\n }\n\n globalThis.AbortSignal = AbortSignal;\n globalThis.AbortController = AbortController;\n })();\n `);\n\n if (result.error) {\n const error = context.dump(result.error);\n result.error.dispose();\n throw new Error(`Failed to setup AbortController: ${JSON.stringify(error)}`);\n }\n result.value.dispose();\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/fetch.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport { unmarshal, marshal } from \"@ricsam/quickjs-core\";\nimport type { RequestState, HeadersState } from \"../types.cjs\";\nimport { headersStateToNative } from \"./headers.cjs\";\nimport { createResponseStateFromNative } from \"./response.cjs\";\n\n/**\n * Create the fetch function for QuickJS\n */\nexport function createFetchFunction(\n context: QuickJSContext,\n onFetch?: (request: Request) => Promise<Response>\n): QuickJSHandle {\n return context.newFunction(\"fetch\", (...argHandles) => {\n // Create promise first\n const deferred = context.newPromise();\n\n const inputHandle = argHandles[0];\n const initHandle = argHandles[1];\n\n // Extract signal BEFORE unmarshalling (keep as handle)\n let signalHandle: QuickJSHandle | null = null;\n // Check if initHandle exists and is a valid QuickJS handle (not JS undefined)\n if (initHandle !== undefined) {\n const initType = context.typeof(initHandle);\n if (initType === \"object\") {\n const tempSignal = context.getProp(initHandle, \"signal\");\n const signalType = context.typeof(tempSignal);\n if (signalType !== \"undefined\" && signalType !== \"null\") {\n signalHandle = tempSignal;\n } else {\n tempSignal.dispose();\n }\n }\n }\n\n // Check if signal is pre-aborted\n let isPreAborted = false;\n let abortReason: { message?: string } | undefined;\n if (signalHandle) {\n const abortedHandle = context.getProp(signalHandle, \"aborted\");\n isPreAborted = Boolean(context.dump(abortedHandle));\n abortedHandle.dispose();\n\n if (isPreAborted) {\n const reasonHandle = context.getProp(signalHandle, \"reason\");\n abortReason = context.dump(reasonHandle) as { message?: string } | undefined;\n reasonHandle.dispose();\n signalHandle.dispose();\n signalHandle = null;\n }\n }\n\n // If pre-aborted, reject immediately in a microtask\n if (isPreAborted) {\n Promise.resolve().then(() => {\n // Create DOMException and reject\n const errorMessage = abortReason?.message || \"signal is aborted without reason\";\n // Escape quotes in the message for safety\n const safeMessage = errorMessage.replace(/\"/g, '\\\\\"');\n const errorResult = context.evalCode(\n `new DOMException(\"${safeMessage}\", \"AbortError\")`\n );\n if (\"value\" in errorResult) {\n deferred.reject(errorResult.value);\n errorResult.value.dispose();\n } else {\n // If evalCode failed, create a plain error\n const fallbackError = marshal(context, { name: \"AbortError\", message: errorMessage });\n deferred.reject(fallbackError);\n fallbackError.dispose();\n errorResult.error.dispose();\n }\n context.runtime.executePendingJobs();\n });\n return deferred.handle;\n }\n\n // Create native AbortController\n const nativeController = new AbortController();\n\n // Wire QuickJS signal to native controller\n if (signalHandle) {\n // Create callback that aborts native controller when QuickJS signal aborts\n const abortCallback = context.newFunction(\"__fetchAbortCallback__\", () => {\n nativeController.abort();\n return context.undefined;\n });\n\n // Call addEventListener on the QuickJS signal\n const addEventListenerHandle = context.getProp(signalHandle, \"addEventListener\");\n const typeHandle = context.newString(\"abort\");\n\n context.callFunction(addEventListenerHandle, signalHandle, typeHandle, abortCallback);\n\n typeHandle.dispose();\n addEventListenerHandle.dispose();\n abortCallback.dispose();\n signalHandle.dispose();\n }\n\n // Unmarshal arguments\n const input = inputHandle ? unmarshal(context, inputHandle) : undefined;\n const init = initHandle ? unmarshal(context, initHandle) as {\n method?: string;\n headers?: object;\n body?: unknown;\n } | undefined : undefined;\n\n // Build the request\n let url = \"\";\n let method = \"GET\";\n let headers = new Headers();\n let body: BodyInit | null = null;\n\n if (typeof input === \"string\") {\n url = input;\n } else if (input && typeof input === \"object\" && \"url\" in input) {\n // Request-like object from QuickJS\n const reqState = input as RequestState;\n url = reqState.url;\n method = reqState.method;\n headers = headersStateToNative(reqState.headersState);\n if (reqState.body) {\n body = reqState.body as BodyInit;\n }\n }\n\n // Apply init overrides\n if (init) {\n if (init.method) {\n method = init.method;\n }\n if (init.headers) {\n if (\n init.headers &&\n typeof init.headers === \"object\" &&\n \"headers\" in init.headers\n ) {\n headers = headersStateToNative(init.headers as HeadersState);\n } else {\n headers = new Headers();\n for (const [key, value] of Object.entries(init.headers)) {\n headers.set(key, String(value));\n }\n }\n }\n if (init.body !== undefined) {\n if (typeof init.body === \"string\") {\n body = init.body;\n } else if (init.body instanceof ArrayBuffer) {\n body = init.body;\n } else if (init.body instanceof Uint8Array) {\n body = init.body as BodyInit;\n } else if (\n init.body &&\n typeof init.body === \"object\" &&\n \"parts\" in init.body\n ) {\n // Blob-like\n const parts = (init.body as { parts: Uint8Array[] }).parts;\n const totalLength = parts.reduce((sum, p) => sum + p.length, 0);\n const combined = new Uint8Array(totalLength);\n let offset = 0;\n for (const part of parts) {\n combined.set(part, offset);\n offset += part.length;\n }\n body = combined as BodyInit;\n }\n }\n }\n\n // Run the async fetch operation in a microtask to ensure proper promise settling\n Promise.resolve().then(async () => {\n try {\n if (!onFetch) {\n throw new Error(\n \"fetch is not configured - no onFetch handler provided to setupFetch\"\n );\n }\n\n // Create native Request with signal\n const nativeRequest = new Request(url, {\n method,\n headers,\n body: method !== \"GET\" && method !== \"HEAD\" ? body : undefined,\n signal: nativeController.signal,\n });\n\n // Call the host fetch handler\n const nativeResponse = await onFetch(nativeRequest);\n\n // Convert to ResponseState for QuickJS\n const responseState = await createResponseStateFromNative(nativeResponse);\n const resultHandle = marshal(context, responseState);\n deferred.resolve(resultHandle);\n resultHandle.dispose();\n } catch (error) {\n // Handle abort errors specially\n if (error instanceof DOMException && error.name === \"AbortError\") {\n const safeMessage = error.message.replace(/\"/g, '\\\\\"');\n const errorResult = context.evalCode(\n `new DOMException(\"${safeMessage}\", \"AbortError\")`\n );\n if (\"value\" in errorResult) {\n deferred.reject(errorResult.value);\n errorResult.value.dispose();\n } else {\n const fallbackError = marshal(context, { name: \"AbortError\", message: error.message });\n deferred.reject(fallbackError);\n fallbackError.dispose();\n errorResult.error.dispose();\n }\n } else {\n const errorHandle = marshal(context, {\n name: error instanceof Error ? error.name : \"Error\",\n message: error instanceof Error ? error.message : String(error),\n });\n deferred.reject(errorHandle);\n errorHandle.dispose();\n }\n }\n context.runtime.executePendingJobs();\n });\n\n return deferred.handle;\n });\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/form-data.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\n\n/**\n * Internal state for FormData entries\n * Each entry can have multiple values (same key appended multiple times)\n */\nexport interface FormDataEntry {\n name: string;\n value: string | FormDataFileValue;\n}\n\nexport interface FormDataFileValue {\n data: Uint8Array;\n filename: string;\n type: string;\n}\n\nexport interface FormDataState {\n entries: FormDataEntry[];\n}\n\nfunction isFileValue(value: unknown): value is FormDataFileValue {\n return (\n value !== null &&\n typeof value === \"object\" &&\n \"data\" in value &&\n \"filename\" in value\n );\n}\n\n/**\n * Create the FormData class for QuickJS\n */\nexport function createFormDataClass(\n context: QuickJSContext,\n stateMap: StateMap\n): QuickJSHandle {\n return defineClass<FormDataState>(context, stateMap, {\n name: \"FormData\",\n construct: () => {\n return { entries: [] };\n },\n methods: {\n append(this: FormDataState, name: unknown, value: unknown, filename?: unknown) {\n const nameStr = String(name);\n\n if (value && typeof value === \"object\" && \"parts\" in value) {\n // Blob-like value\n const blobLike = value as { parts: Uint8Array[]; type?: string; name?: string };\n const data = concatenateParts(blobLike.parts);\n const fileValue: FormDataFileValue = {\n data,\n filename: filename !== undefined ? String(filename) : (blobLike.name || \"blob\"),\n type: blobLike.type || \"application/octet-stream\",\n };\n this.entries.push({ name: nameStr, value: fileValue });\n } else if (value && typeof value === \"object\" && \"data\" in value && \"filename\" in value) {\n // Already a FormDataFileValue\n this.entries.push({\n name: nameStr,\n value: {\n ...(value as FormDataFileValue),\n filename: filename !== undefined ? String(filename) : (value as FormDataFileValue).filename,\n },\n });\n } else {\n // String value\n this.entries.push({ name: nameStr, value: String(value) });\n }\n },\n delete(this: FormDataState, name: unknown) {\n const nameStr = String(name);\n this.entries = this.entries.filter((e) => e.name !== nameStr);\n },\n get(this: FormDataState, name: unknown): string | FormDataFileValue | null {\n const nameStr = String(name);\n const entry = this.entries.find((e) => e.name === nameStr);\n return entry ? entry.value : null;\n },\n getAll(this: FormDataState, name: unknown): Array<string | FormDataFileValue> {\n const nameStr = String(name);\n return this.entries\n .filter((e) => e.name === nameStr)\n .map((e) => e.value);\n },\n has(this: FormDataState, name: unknown): boolean {\n const nameStr = String(name);\n return this.entries.some((e) => e.name === nameStr);\n },\n set(this: FormDataState, name: unknown, value: unknown, filename?: unknown) {\n const nameStr = String(name);\n // Remove all existing entries with this name\n this.entries = this.entries.filter((e) => e.name !== nameStr);\n\n // Add the new entry (using append logic)\n if (value && typeof value === \"object\" && \"parts\" in value) {\n const blobLike = value as { parts: Uint8Array[]; type?: string; name?: string };\n const data = concatenateParts(blobLike.parts);\n const fileValue: FormDataFileValue = {\n data,\n filename: filename !== undefined ? String(filename) : (blobLike.name || \"blob\"),\n type: blobLike.type || \"application/octet-stream\",\n };\n this.entries.push({ name: nameStr, value: fileValue });\n } else if (value && typeof value === \"object\" && \"data\" in value && \"filename\" in value) {\n this.entries.push({\n name: nameStr,\n value: {\n ...(value as FormDataFileValue),\n filename: filename !== undefined ? String(filename) : (value as FormDataFileValue).filename,\n },\n });\n } else {\n this.entries.push({ name: nameStr, value: String(value) });\n }\n },\n entries(this: FormDataState): Array<[string, string | FormDataFileValue]> {\n return this.entries.map((e) => [e.name, e.value]);\n },\n keys(this: FormDataState): string[] {\n // Return unique keys in order of first appearance\n const seen = new Set<string>();\n const result: string[] = [];\n for (const entry of this.entries) {\n if (!seen.has(entry.name)) {\n seen.add(entry.name);\n result.push(entry.name);\n }\n }\n return result;\n },\n values(this: FormDataState): Array<string | FormDataFileValue> {\n return this.entries.map((e) => e.value);\n },\n forEach(this: FormDataState, callback: unknown) {\n if (typeof callback !== \"function\") {\n throw new TypeError(\"callback must be a function\");\n }\n for (const entry of this.entries) {\n (callback as (value: string | FormDataFileValue, key: string, parent: FormDataState) => void)(\n entry.value,\n entry.name,\n this\n );\n }\n },\n },\n });\n}\n\n/**\n * Concatenate Uint8Arrays into a single Uint8Array\n */\nfunction concatenateParts(parts: Uint8Array[]): Uint8Array {\n const totalLength = parts.reduce((sum, part) => sum + part.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const part of parts) {\n result.set(part, offset);\n offset += part.length;\n }\n return result;\n}\n\n/**\n * Parse multipart/form-data body\n */\nexport function parseMultipartFormData(\n body: Uint8Array,\n contentType: string\n): FormDataState {\n const entries: FormDataEntry[] = [];\n\n // Extract boundary from content-type\n const boundaryMatch = contentType.match(/boundary=([^;]+)/i);\n if (!boundaryMatch || !boundaryMatch[1]) {\n return { entries };\n }\n\n const boundary = boundaryMatch[1].replace(/^[\"']|[\"']$/g, \"\");\n const boundaryBytes = new TextEncoder().encode(`--${boundary}`);\n const endBoundaryBytes = new TextEncoder().encode(`--${boundary}--`);\n\n // Find all parts\n const decoder = new TextDecoder();\n let pos = 0;\n\n // Skip preamble and first boundary\n pos = findSequence(body, boundaryBytes, pos);\n if (pos === -1) return { entries };\n pos += boundaryBytes.length;\n\n while (pos < body.length) {\n // Skip CRLF after boundary\n if (body[pos] === 0x0d && body[pos + 1] === 0x0a) {\n pos += 2;\n } else if (body[pos] === 0x0a) {\n pos += 1;\n }\n\n // Check for end boundary\n if (pos + 2 <= body.length && body[pos] === 0x2d && body[pos + 1] === 0x2d) {\n break; // End of multipart\n }\n\n // Parse headers\n const headersEnd = findSequence(body, new Uint8Array([0x0d, 0x0a, 0x0d, 0x0a]), pos);\n if (headersEnd === -1) break;\n\n const headersText = decoder.decode(body.slice(pos, headersEnd));\n const headers = parseHeaders(headersText);\n pos = headersEnd + 4;\n\n // Find next boundary\n const nextBoundary = findSequence(body, boundaryBytes, pos);\n if (nextBoundary === -1) break;\n\n // Content is between current position and next boundary (minus CRLF)\n let contentEnd = nextBoundary;\n if (contentEnd > 0 && body[contentEnd - 1] === 0x0a) contentEnd--;\n if (contentEnd > 0 && body[contentEnd - 1] === 0x0d) contentEnd--;\n\n const content = body.slice(pos, contentEnd);\n\n // Parse Content-Disposition header\n const disposition = headers[\"content-disposition\"] || \"\";\n const nameMatch = disposition.match(/name=\"([^\"]+)\"/);\n const filenameMatch = disposition.match(/filename=\"([^\"]+)\"/);\n\n if (nameMatch && nameMatch[1]) {\n const name = nameMatch[1];\n if (filenameMatch && filenameMatch[1]) {\n // File entry\n const filename = filenameMatch[1];\n const type = headers[\"content-type\"] || \"application/octet-stream\";\n entries.push({\n name,\n value: { data: content, filename, type },\n });\n } else {\n // String entry\n entries.push({\n name,\n value: decoder.decode(content),\n });\n }\n }\n\n pos = nextBoundary + boundaryBytes.length;\n }\n\n return { entries };\n}\n\n/**\n * Parse URL-encoded form data\n */\nexport function parseUrlEncodedFormData(body: Uint8Array): FormDataState {\n const text = new TextDecoder().decode(body);\n const entries: FormDataEntry[] = [];\n\n const params = new URLSearchParams(text);\n for (const [name, value] of params) {\n entries.push({ name, value });\n }\n\n return { entries };\n}\n\nfunction findSequence(haystack: Uint8Array, needle: Uint8Array, start: number): number {\n outer: for (let i = start; i <= haystack.length - needle.length; i++) {\n for (let j = 0; j < needle.length; j++) {\n if (haystack[i + j] !== needle[j]) continue outer;\n }\n return i;\n }\n return -1;\n}\n\nfunction parseHeaders(text: string): Record<string, string> {\n const headers: Record<string, string> = {};\n const lines = text.split(/\\r?\\n/);\n for (const line of lines) {\n const colonIndex = line.indexOf(\":\");\n if (colonIndex > 0) {\n const name = line.slice(0, colonIndex).trim().toLowerCase();\n const value = line.slice(colonIndex + 1).trim();\n headers[name] = value;\n }\n }\n return headers;\n}\n\n/**\n * Serialize FormData to multipart/form-data format\n */\nexport function serializeFormData(state: FormDataState): { body: Uint8Array; contentType: string } {\n const boundary = `----FormDataBoundary${Math.random().toString(36).slice(2)}`;\n const encoder = new TextEncoder();\n const parts: Uint8Array[] = [];\n\n for (const entry of state.entries) {\n const headerLines: string[] = [];\n headerLines.push(`--${boundary}`);\n\n if (isFileValue(entry.value)) {\n headerLines.push(\n `Content-Disposition: form-data; name=\"${entry.name}\"; filename=\"${entry.value.filename}\"`\n );\n headerLines.push(`Content-Type: ${entry.value.type}`);\n headerLines.push(\"\");\n\n parts.push(encoder.encode(headerLines.join(\"\\r\\n\") + \"\\r\\n\"));\n parts.push(entry.value.data);\n parts.push(encoder.encode(\"\\r\\n\"));\n } else {\n headerLines.push(`Content-Disposition: form-data; name=\"${entry.name}\"`);\n headerLines.push(\"\");\n headerLines.push(entry.value);\n\n parts.push(encoder.encode(headerLines.join(\"\\r\\n\") + \"\\r\\n\"));\n }\n }\n\n parts.push(encoder.encode(`--${boundary}--\\r\\n`));\n\n // Concatenate all parts\n const totalLength = parts.reduce((sum, part) => sum + part.length, 0);\n const body = new Uint8Array(totalLength);\n let offset = 0;\n for (const part of parts) {\n body.set(part, offset);\n offset += part.length;\n }\n\n return {\n body,\n contentType: `multipart/form-data; boundary=${boundary}`,\n };\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/headers.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\nimport type { HeadersState } from \"../types.cjs\";\n\n/**\n * Create the Headers class for QuickJS\n */\nexport function createHeadersClass(\n context: QuickJSContext,\n stateMap: StateMap\n): QuickJSHandle {\n return defineClass<HeadersState>(context, stateMap, {\n name: \"Headers\",\n construct: (args) => {\n const init = args[0];\n const headers = new Map<string, string[]>();\n\n if (init) {\n if (Array.isArray(init)) {\n // Array of [key, value] pairs\n for (const pair of init) {\n if (Array.isArray(pair) && pair.length >= 2) {\n const key = String(pair[0]).toLowerCase();\n const value = String(pair[1]);\n const existing = headers.get(key) || [];\n existing.push(value);\n headers.set(key, existing);\n }\n }\n } else if (typeof init === \"object\" && init !== null) {\n // Check if it's another Headers-like object with entries method\n if (\"headers\" in init && init.headers instanceof Map) {\n // HeadersState\n for (const [key, values] of (init as HeadersState).headers) {\n headers.set(key, [...values]);\n }\n } else {\n // Plain object\n for (const [key, value] of Object.entries(init)) {\n const normalizedKey = key.toLowerCase();\n if (Array.isArray(value)) {\n headers.set(normalizedKey, value.map(String));\n } else {\n headers.set(normalizedKey, [String(value)]);\n }\n }\n }\n }\n }\n\n return { headers };\n },\n methods: {\n append(this: HeadersState, name: unknown, value: unknown) {\n const key = String(name).toLowerCase();\n const existing = this.headers.get(key) || [];\n existing.push(String(value));\n this.headers.set(key, existing);\n },\n delete(this: HeadersState, name: unknown) {\n this.headers.delete(String(name).toLowerCase());\n },\n get(this: HeadersState, name: unknown): string | null {\n const values = this.headers.get(String(name).toLowerCase());\n return values ? values.join(\", \") : null;\n },\n has(this: HeadersState, name: unknown): boolean {\n return this.headers.has(String(name).toLowerCase());\n },\n set(this: HeadersState, name: unknown, value: unknown) {\n this.headers.set(String(name).toLowerCase(), [String(value)]);\n },\n entries(this: HeadersState): Array<[string, string]> {\n const result: Array<[string, string]> = [];\n for (const [key, values] of this.headers) {\n result.push([key, values.join(\", \")]);\n }\n return result;\n },\n keys(this: HeadersState): string[] {\n return Array.from(this.headers.keys());\n },\n values(this: HeadersState): string[] {\n const result: string[] = [];\n for (const values of this.headers.values()) {\n result.push(values.join(\", \"));\n }\n return result;\n },\n forEach(this: HeadersState, callback: unknown) {\n if (typeof callback !== \"function\") {\n throw new TypeError(\"callback must be a function\");\n }\n for (const [key, values] of this.headers) {\n (callback as (value: string, key: string, parent: HeadersState) => void)(\n values.join(\", \"),\n key,\n this\n );\n }\n },\n getSetCookie(this: HeadersState): string[] {\n return this.headers.get(\"set-cookie\") || [];\n },\n },\n });\n}\n\n/**\n * Create a HeadersState from a native Headers object\n */\nexport function createHeadersStateFromNative(headers: Headers): HeadersState {\n const map = new Map<string, string[]>();\n headers.forEach((value, key) => {\n const existing = map.get(key.toLowerCase()) || [];\n existing.push(value);\n map.set(key.toLowerCase(), existing);\n });\n return { headers: map };\n}\n\n/**\n * Convert HeadersState to native Headers\n */\nexport function headersStateToNative(state: HeadersState): Headers {\n const headers = new Headers();\n for (const [key, values] of state.headers) {\n for (const value of values) {\n headers.append(key, value);\n }\n }\n return headers;\n}\n\n/**\n * Interface for a Headers-like object that can be returned from getters\n */\nexport interface HeadersLike {\n headers: Map<string, string[]>;\n append(name: string, value: string): void;\n delete(name: string): void;\n get(name: string): string | null;\n has(name: string): boolean;\n set(name: string, value: string): void;\n entries(): Array<[string, string]>;\n keys(): string[];\n values(): string[];\n forEach(callback: (value: string, key: string) => void): void;\n getSetCookie(): string[];\n}\n\n/**\n * Create a Headers-like object from HeadersState that has all the Headers methods\n * This is used for getters that need to return an object with Headers API\n */\nexport function createHeadersLike(state: HeadersState): HeadersLike {\n return {\n headers: state.headers,\n append(name: string, value: string) {\n const key = name.toLowerCase();\n const existing = state.headers.get(key) || [];\n existing.push(value);\n state.headers.set(key, existing);\n },\n delete(name: string) {\n state.headers.delete(name.toLowerCase());\n },\n get(name: string): string | null {\n const values = state.headers.get(name.toLowerCase());\n return values ? values.join(\", \") : null;\n },\n has(name: string): boolean {\n return state.headers.has(name.toLowerCase());\n },\n set(name: string, value: string) {\n state.headers.set(name.toLowerCase(), [value]);\n },\n entries(): Array<[string, string]> {\n const result: Array<[string, string]> = [];\n for (const [key, values] of state.headers) {\n result.push([key, values.join(\", \")]);\n }\n return result;\n },\n keys(): string[] {\n return Array.from(state.headers.keys());\n },\n values(): string[] {\n const result: string[] = [];\n for (const values of state.headers.values()) {\n result.push(values.join(\", \"));\n }\n return result;\n },\n forEach(callback: (value: string, key: string) => void) {\n for (const [key, values] of state.headers) {\n callback(values.join(\", \"), key);\n }\n },\n getSetCookie(): string[] {\n return state.headers.get(\"set-cookie\") || [];\n },\n };\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/request.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\nimport type { RequestState, HeadersState, AbortSignalState, FormDataState } from \"../types.cjs\";\nimport { createHeadersStateFromNative, createHeadersLike } from \"./headers.cjs\";\nimport { parseMultipartFormData, parseUrlEncodedFormData } from \"./form-data.cjs\";\n\n/**\n * Type for the stream factory function\n */\ntype StreamFactory = (source: UnderlyingSource) => QuickJSHandle;\n\n/**\n * Create the Request class for QuickJS\n */\nexport function createRequestClass(\n context: QuickJSContext,\n stateMap: StateMap,\n createStream?: StreamFactory\n): QuickJSHandle {\n return defineClass<RequestState>(context, stateMap, {\n name: \"Request\",\n construct: (args) => {\n const input = args[0];\n const init = args[1] as {\n method?: string;\n headers?: object;\n body?: unknown;\n cache?: string;\n credentials?: string;\n integrity?: string;\n keepalive?: boolean;\n mode?: string;\n redirect?: string;\n referrer?: string;\n referrerPolicy?: string;\n signal?: AbortSignalState;\n } | undefined;\n\n let url = \"\";\n let method = \"GET\";\n let headersState: HeadersState = { headers: new Map() };\n let body: Uint8Array | null = null;\n let signal: AbortSignalState | null = null;\n\n // Handle input\n if (typeof input === \"string\") {\n url = input;\n } else if (input && typeof input === \"object\") {\n // Could be URL or Request-like\n if (\"url\" in input) {\n url = String((input as { url: string }).url);\n }\n if (\"method\" in input) {\n method = String((input as { method: string }).method);\n }\n if (\"headersState\" in input) {\n const inputHeaders = (input as RequestState).headersState;\n headersState = {\n headers: new Map(inputHeaders.headers),\n };\n }\n if (\"body\" in input && (input as RequestState).body) {\n body = (input as RequestState).body;\n }\n if (\"signal\" in input) {\n signal = (input as RequestState).signal;\n }\n }\n\n // Apply init options\n if (init) {\n if (init.method) {\n method = init.method.toUpperCase();\n }\n if (init.headers) {\n if (init.headers && typeof init.headers === \"object\") {\n if (\"headers\" in init.headers && init.headers.headers instanceof Map) {\n headersState = {\n headers: new Map((init.headers as HeadersState).headers),\n };\n } else {\n headersState = { headers: new Map() };\n for (const [key, value] of Object.entries(init.headers)) {\n headersState.headers.set(key.toLowerCase(), [String(value)]);\n }\n }\n }\n }\n if (init.body !== undefined && init.body !== null) {\n if (typeof init.body === \"string\") {\n body = new TextEncoder().encode(init.body);\n } else if (init.body instanceof ArrayBuffer) {\n body = new Uint8Array(init.body);\n } else if (init.body instanceof Uint8Array) {\n body = init.body;\n }\n }\n if (init.signal) {\n signal = init.signal;\n }\n }\n\n return {\n method,\n url,\n headersState,\n body,\n bodyUsed: false,\n cache: init?.cache || \"default\",\n credentials: init?.credentials || \"same-origin\",\n destination: \"\",\n integrity: init?.integrity || \"\",\n keepalive: init?.keepalive || false,\n mode: init?.mode || \"cors\",\n redirect: init?.redirect || \"follow\",\n referrer: init?.referrer || \"about:client\",\n referrerPolicy: init?.referrerPolicy || \"\",\n signal,\n };\n },\n properties: {\n method: {\n get(this: RequestState) {\n return this.method;\n },\n },\n url: {\n get(this: RequestState) {\n return this.url;\n },\n },\n headers: {\n get(this: RequestState) {\n return createHeadersLike(this.headersState);\n },\n },\n body: {\n get(this: RequestState) {\n if (!this.body) return null;\n if (!createStream) {\n // Fallback: return raw body if no stream factory\n return this.body;\n }\n // Create a ReadableStream from the body data\n const bodyData = this.body;\n let offset = 0;\n const chunkSize = 65536; // 64KB chunks\n return createStream({\n pull(controller) {\n if (offset >= bodyData.length) {\n controller.close();\n return;\n }\n const chunk = bodyData.slice(offset, Math.min(offset + chunkSize, bodyData.length));\n offset += chunk.length;\n controller.enqueue(chunk);\n },\n });\n },\n },\n bodyUsed: {\n get(this: RequestState) {\n return this.bodyUsed;\n },\n },\n cache: {\n get(this: RequestState) {\n return this.cache;\n },\n },\n credentials: {\n get(this: RequestState) {\n return this.credentials;\n },\n },\n destination: {\n get(this: RequestState) {\n return this.destination;\n },\n },\n integrity: {\n get(this: RequestState) {\n return this.integrity;\n },\n },\n keepalive: {\n get(this: RequestState) {\n return this.keepalive;\n },\n },\n mode: {\n get(this: RequestState) {\n return this.mode;\n },\n },\n redirect: {\n get(this: RequestState) {\n return this.redirect;\n },\n },\n referrer: {\n get(this: RequestState) {\n return this.referrer;\n },\n },\n referrerPolicy: {\n get(this: RequestState) {\n return this.referrerPolicy;\n },\n },\n signal: {\n get(this: RequestState) {\n return this.signal;\n },\n },\n },\n methods: {\n async arrayBuffer(this: RequestState): Promise<ArrayBuffer> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return new ArrayBuffer(0);\n }\n return this.body.buffer.slice(\n this.body.byteOffset,\n this.body.byteOffset + this.body.byteLength\n ) as ArrayBuffer;\n },\n async blob(this: RequestState): Promise<object> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n const contentType = this.headersState.headers.get(\"content-type\")?.[0] || \"\";\n return {\n parts: this.body ? [this.body] : [],\n type: contentType,\n size: this.body?.length || 0,\n };\n },\n clone(this: RequestState): RequestState {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n return {\n ...this,\n headersState: {\n headers: new Map(this.headersState.headers),\n },\n body: this.body ? new Uint8Array(this.body) : null,\n bodyUsed: false,\n };\n },\n async json(this: RequestState): Promise<unknown> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return JSON.parse(\"\");\n }\n const text = new TextDecoder().decode(this.body);\n return JSON.parse(text);\n },\n async text(this: RequestState): Promise<string> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return \"\";\n }\n return new TextDecoder().decode(this.body);\n },\n async formData(this: RequestState): Promise<FormDataState> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return { entries: [] };\n }\n\n const contentType = this.headersState.headers.get(\"content-type\")?.[0] || \"\";\n\n if (contentType.includes(\"multipart/form-data\")) {\n return parseMultipartFormData(this.body, contentType);\n } else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n return parseUrlEncodedFormData(this.body);\n }\n\n throw new TypeError(\"Could not parse content as FormData\");\n },\n },\n });\n}\n\n/**\n * Create a RequestState from a native Request object\n */\nexport async function createRequestStateFromNative(\n request: Request\n): Promise<RequestState> {\n const body = request.body\n ? new Uint8Array(await request.arrayBuffer())\n : null;\n\n return {\n method: request.method,\n url: request.url,\n headersState: createHeadersStateFromNative(request.headers),\n body,\n bodyUsed: false,\n cache: request.cache,\n credentials: request.credentials,\n destination: request.destination,\n integrity: request.integrity,\n keepalive: request.keepalive,\n mode: request.mode,\n redirect: request.redirect,\n referrer: request.referrer,\n referrerPolicy: request.referrerPolicy,\n signal: null, // Signal handling is complex, simplified here\n };\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/response.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\nimport type { ResponseState, HeadersState, FormDataState } from \"../types.cjs\";\nimport { headersStateToNative, createHeadersLike } from \"./headers.cjs\";\nimport { parseMultipartFormData, parseUrlEncodedFormData } from \"./form-data.cjs\";\n\n/**\n * Type for the stream factory function\n */\ntype StreamFactory = (source: UnderlyingSource) => QuickJSHandle;\n\n/**\n * Create the Response class for QuickJS\n */\nexport function createResponseClass(\n context: QuickJSContext,\n stateMap: StateMap,\n createStream?: StreamFactory\n): QuickJSHandle {\n const classHandle = defineClass<ResponseState>(context, stateMap, {\n name: \"Response\",\n construct: (args) => {\n const bodyInit = args[0];\n const init = args[1] as {\n status?: number;\n statusText?: string;\n headers?: object;\n _type?: string; // Internal: for Response.error() to set type\n } | undefined;\n\n let body: Uint8Array | null = null;\n const status = init?.status ?? 200;\n const statusText = init?.statusText ?? \"\";\n let headersState: HeadersState = { headers: new Map() };\n\n // Parse body\n if (bodyInit !== null && bodyInit !== undefined) {\n if (typeof bodyInit === \"string\") {\n body = new TextEncoder().encode(bodyInit);\n } else if (bodyInit instanceof ArrayBuffer) {\n body = new Uint8Array(bodyInit);\n } else if (bodyInit instanceof Uint8Array) {\n body = bodyInit;\n } else if (\n bodyInit &&\n typeof bodyInit === \"object\" &&\n \"parts\" in bodyInit\n ) {\n // Blob-like\n const parts = (bodyInit as { parts: Uint8Array[] }).parts;\n const totalLength = parts.reduce((sum, p) => sum + p.length, 0);\n body = new Uint8Array(totalLength);\n let offset = 0;\n for (const part of parts) {\n body.set(part, offset);\n offset += part.length;\n }\n }\n }\n\n // Parse headers\n if (init?.headers) {\n if (\n init.headers &&\n typeof init.headers === \"object\" &&\n \"headers\" in init.headers &&\n init.headers.headers instanceof Map\n ) {\n headersState = {\n headers: new Map((init.headers as HeadersState).headers),\n };\n } else {\n headersState = { headers: new Map() };\n for (const [key, value] of Object.entries(init.headers)) {\n headersState.headers.set(key.toLowerCase(), [String(value)]);\n }\n }\n }\n\n return {\n status,\n statusText,\n headersState,\n body,\n bodyUsed: false,\n url: \"\",\n redirected: false,\n type: init?._type ?? \"default\",\n ok: status >= 200 && status < 300,\n };\n },\n properties: {\n body: {\n get(this: ResponseState) {\n if (!this.body) return null;\n if (!createStream) {\n // Fallback: return raw body if no stream factory\n return this.body;\n }\n // Create a ReadableStream from the body data\n const bodyData = this.body;\n let offset = 0;\n const chunkSize = 65536; // 64KB chunks\n return createStream({\n pull(controller) {\n if (offset >= bodyData.length) {\n controller.close();\n return;\n }\n const chunk = bodyData.slice(offset, Math.min(offset + chunkSize, bodyData.length));\n offset += chunk.length;\n controller.enqueue(chunk);\n },\n });\n },\n },\n bodyUsed: {\n get(this: ResponseState) {\n return this.bodyUsed;\n },\n },\n headers: {\n get(this: ResponseState) {\n return createHeadersLike(this.headersState);\n },\n },\n ok: {\n get(this: ResponseState) {\n return this.ok;\n },\n },\n redirected: {\n get(this: ResponseState) {\n return this.redirected;\n },\n },\n status: {\n get(this: ResponseState) {\n return this.status;\n },\n },\n statusText: {\n get(this: ResponseState) {\n return this.statusText;\n },\n },\n type: {\n get(this: ResponseState) {\n return this.type;\n },\n },\n url: {\n get(this: ResponseState) {\n return this.url;\n },\n },\n },\n methods: {\n async arrayBuffer(this: ResponseState): Promise<ArrayBuffer> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return new ArrayBuffer(0);\n }\n return this.body.buffer.slice(\n this.body.byteOffset,\n this.body.byteOffset + this.body.byteLength\n ) as ArrayBuffer;\n },\n async blob(this: ResponseState): Promise<object> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n const contentType =\n this.headersState.headers.get(\"content-type\")?.[0] || \"\";\n return {\n parts: this.body ? [this.body] : [],\n type: contentType,\n size: this.body?.length || 0,\n };\n },\n clone(this: ResponseState): ResponseState {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n return {\n ...this,\n headersState: {\n headers: new Map(this.headersState.headers),\n },\n body: this.body ? new Uint8Array(this.body) : null,\n bodyUsed: false,\n };\n },\n async json(this: ResponseState): Promise<unknown> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return JSON.parse(\"\");\n }\n const text = new TextDecoder().decode(this.body);\n return JSON.parse(text);\n },\n async text(this: ResponseState): Promise<string> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return \"\";\n }\n return new TextDecoder().decode(this.body);\n },\n async formData(this: ResponseState): Promise<FormDataState> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return { entries: [] };\n }\n\n const contentType = this.headersState.headers.get(\"content-type\")?.[0] || \"\";\n\n if (contentType.includes(\"multipart/form-data\")) {\n return parseMultipartFormData(this.body, contentType);\n } else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n return parseUrlEncodedFormData(this.body);\n }\n\n throw new TypeError(\"Could not parse content as FormData\");\n },\n },\n });\n\n return classHandle;\n}\n\n/**\n * Add static methods to Response class after it's been set on global\n * This must be called after Response and Headers are available on global\n */\nexport function addResponseStaticMethods(context: QuickJSContext): void {\n const staticMethodsCode = `\n Response.error = function() {\n return new Response(null, { status: 0, _type: \"error\" });\n };\n\n Response.json = function(data, init = {}) {\n const body = JSON.stringify(data);\n // Merge content-type with any provided headers\n const headers = Object.assign(\n { \"content-type\": \"application/json\" },\n init.headers || {}\n );\n return new Response(body, {\n status: init.status ?? 200,\n statusText: init.statusText ?? \"\",\n headers: headers\n });\n };\n\n Response.redirect = function(url, status = 302) {\n return new Response(null, {\n status: status,\n headers: { \"location\": String(url) }\n });\n };\n `;\n const result = context.evalCode(staticMethodsCode);\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n}\n\n/**\n * Convert ResponseState (or unmarshalled Response object) to native Response\n */\nexport function responseStateToNative(state: ResponseState | Record<string, unknown>): Response {\n const body = (state as ResponseState).body ?? (state as Record<string, unknown>).body;\n const status = (state as ResponseState).status ?? 200;\n const statusText = (state as ResponseState).statusText ?? \"\";\n\n // Handle both headersState (internal) and headers (from getter)\n let headersState: HeadersState;\n if ((state as ResponseState).headersState) {\n headersState = (state as ResponseState).headersState;\n } else if ((state as Record<string, unknown>).headers) {\n // When unmarshalled, headers is the HeadersLike object from getter\n const headers = (state as Record<string, unknown>).headers as { headers?: Map<string, string[]> };\n if (headers.headers instanceof Map) {\n headersState = { headers: headers.headers };\n } else {\n headersState = { headers: new Map() };\n }\n } else {\n headersState = { headers: new Map() };\n }\n\n return new Response(body as BodyInit | null, {\n status,\n statusText,\n headers: headersStateToNative(headersState),\n });\n}\n\n/**\n * Create a ResponseState from a native Response\n */\nexport async function createResponseStateFromNative(\n response: Response\n): Promise<ResponseState> {\n const body = response.body\n ? new Uint8Array(await response.arrayBuffer())\n : null;\n\n const headersState: HeadersState = { headers: new Map() };\n response.headers.forEach((value, key) => {\n const existing = headersState.headers.get(key.toLowerCase()) || [];\n existing.push(value);\n headersState.headers.set(key.toLowerCase(), existing);\n });\n\n return {\n status: response.status,\n statusText: response.statusText,\n headersState,\n body,\n bodyUsed: false,\n url: response.url,\n redirected: response.redirected,\n type: response.type,\n ok: response.ok,\n };\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/serve.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\nimport type { ServeState, ServerWebSocketState, WebSocketCommand } from \"../types.cjs\";\n\ninterface ServerState {\n serveState: ServeState;\n}\n\n/**\n * Create the ServerWebSocket class for QuickJS\n */\nexport function createServerWebSocketClass(\n context: QuickJSContext,\n stateMap: StateMap,\n onWebSocketCommand: (cmd: WebSocketCommand) => void\n): QuickJSHandle {\n return defineClass<ServerWebSocketState>(context, stateMap, {\n name: \"ServerWebSocket\",\n construct: (args) => {\n const connectionId = String(args[0]);\n const data = args[1];\n\n return {\n data,\n readyState: 1, // OPEN\n connectionId,\n wsHandle: null,\n };\n },\n properties: {\n data: {\n get(this: ServerWebSocketState) {\n return this.data;\n },\n },\n readyState: {\n get(this: ServerWebSocketState) {\n return this.readyState;\n },\n },\n },\n methods: {\n send(this: ServerWebSocketState, message: unknown) {\n if (this.readyState !== 1) {\n throw new Error(\"WebSocket is not open\");\n }\n\n let data: string | ArrayBuffer;\n if (typeof message === \"string\") {\n data = message;\n } else if (message instanceof ArrayBuffer) {\n data = message;\n } else if (message instanceof Uint8Array) {\n data = message.buffer.slice(\n message.byteOffset,\n message.byteOffset + message.byteLength\n ) as ArrayBuffer;\n } else {\n data = String(message);\n }\n\n onWebSocketCommand({\n type: \"message\",\n connectionId: this.connectionId,\n data,\n });\n },\n close(this: ServerWebSocketState, code?: unknown, reason?: unknown) {\n if (this.readyState === 3) {\n return; // Already closed\n }\n\n this.readyState = 2; // CLOSING\n\n onWebSocketCommand({\n type: \"close\",\n connectionId: this.connectionId,\n code: typeof code === \"number\" ? code : undefined,\n reason: typeof reason === \"string\" ? reason : undefined,\n });\n },\n },\n });\n}\n\n/**\n * Create the Server class for QuickJS\n */\nexport function createServerClass(\n context: QuickJSContext,\n stateMap: StateMap,\n serveState: ServeState\n): QuickJSHandle {\n return defineClass<ServerState>(context, stateMap, {\n name: \"Server\",\n construct: () => {\n return { serveState };\n },\n methods: {\n upgrade(\n this: ServerState,\n _request: unknown,\n options?: unknown\n ): boolean {\n const data =\n options && typeof options === \"object\" && \"data\" in options\n ? (options as { data: unknown }).data\n : undefined;\n\n this.serveState.pendingUpgrade = {\n requested: true,\n data,\n };\n\n return true;\n },\n },\n });\n}\n\n/**\n * Create the serve function for QuickJS\n * Uses context.newFunction instead of defineFunction to properly manage handle lifetimes\n */\nexport function createServeFunction(\n context: QuickJSContext,\n stateMap: StateMap,\n serveState: ServeState\n): QuickJSHandle {\n return context.newFunction(\"serve\", (optionsHandle) => {\n if (context.typeof(optionsHandle) !== \"object\") {\n throw context.newError(\"serve requires an options object\");\n }\n\n // Dispose previous handlers if any\n if (serveState.fetchHandler) {\n serveState.fetchHandler.dispose();\n serveState.fetchHandler = null;\n }\n\n // Get and store fetch handler - don't dispose, keep alive for later calls\n const fetchHandle = context.getProp(optionsHandle, \"fetch\");\n if (context.typeof(fetchHandle) === \"function\") {\n serveState.fetchHandler = fetchHandle;\n } else {\n fetchHandle.dispose();\n }\n\n // Get and store websocket handlers\n const websocketHandle = context.getProp(optionsHandle, \"websocket\");\n if (context.typeof(websocketHandle) === \"object\") {\n // Dispose previous handlers\n if (serveState.websocketHandlers.open) {\n serveState.websocketHandlers.open.dispose();\n serveState.websocketHandlers.open = undefined;\n }\n if (serveState.websocketHandlers.message) {\n serveState.websocketHandlers.message.dispose();\n serveState.websocketHandlers.message = undefined;\n }\n if (serveState.websocketHandlers.close) {\n serveState.websocketHandlers.close.dispose();\n serveState.websocketHandlers.close = undefined;\n }\n if (serveState.websocketHandlers.error) {\n serveState.websocketHandlers.error.dispose();\n serveState.websocketHandlers.error = undefined;\n }\n\n const openHandle = context.getProp(websocketHandle, \"open\");\n if (context.typeof(openHandle) === \"function\") {\n serveState.websocketHandlers.open = openHandle;\n } else {\n openHandle.dispose();\n }\n\n const messageHandle = context.getProp(websocketHandle, \"message\");\n if (context.typeof(messageHandle) === \"function\") {\n serveState.websocketHandlers.message = messageHandle;\n } else {\n messageHandle.dispose();\n }\n\n const closeHandle = context.getProp(websocketHandle, \"close\");\n if (context.typeof(closeHandle) === \"function\") {\n serveState.websocketHandlers.close = closeHandle;\n } else {\n closeHandle.dispose();\n }\n\n const errorHandle = context.getProp(websocketHandle, \"error\");\n if (context.typeof(errorHandle) === \"function\") {\n serveState.websocketHandlers.error = errorHandle;\n } else {\n errorHandle.dispose();\n }\n }\n websocketHandle.dispose();\n\n return context.undefined;\n });\n}\n"
|
|
6
6
|
],
|
package/dist/cjs/handle.cjs
CHANGED
|
@@ -84,7 +84,17 @@ function createFetchHandle(context, stateMap, serveState) {
|
|
|
84
84
|
const hasThen = context.typeof(thenHandle) === "function";
|
|
85
85
|
thenHandle.dispose();
|
|
86
86
|
if (hasThen) {
|
|
87
|
-
|
|
87
|
+
context.runtime.executePendingJobs();
|
|
88
|
+
const resolved = await Promise.race([
|
|
89
|
+
context.resolvePromise(responseHandle),
|
|
90
|
+
(async () => {
|
|
91
|
+
for (let i = 0;i < 1000; i++) {
|
|
92
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
93
|
+
context.runtime.executePendingJobs();
|
|
94
|
+
}
|
|
95
|
+
throw new Error("Promise resolution timeout");
|
|
96
|
+
})()
|
|
97
|
+
]);
|
|
88
98
|
responseHandle.dispose();
|
|
89
99
|
context.runtime.executePendingJobs();
|
|
90
100
|
if (resolved.error) {
|
|
@@ -245,4 +255,4 @@ function createFetchHandle(context, stateMap, serveState) {
|
|
|
245
255
|
}
|
|
246
256
|
})
|
|
247
257
|
|
|
248
|
-
//# debugId=
|
|
258
|
+
//# debugId=CF7F460488EA084964756E2164756E21
|
package/dist/cjs/handle.cjs.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/handle.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport { marshal, getInstanceState, setInstanceState } from \"@ricsam/quickjs-core\";\nimport type {\n StateMap,\n FetchHandle,\n ServeState,\n UpgradeRequest,\n WebSocketCommand,\n ServerWebSocketState,\n RequestState,\n ResponseState,\n} from \"./types.cjs\";\nimport { createRequestStateFromNative } from \"./globals/request.cjs\";\nimport { responseStateToNative } from \"./globals/response.cjs\";\n\n/**\n * Create a proper Request instance in QuickJS with the given state\n */\nfunction createRequestInstance(\n context: QuickJSContext,\n stateMap: StateMap,\n requestState: RequestState\n): QuickJSHandle {\n // Create the Request instance using evalCode (classes must be called with 'new')\n // Pass the URL as the constructor argument - minimal construction\n const urlHandle = context.newString(requestState.url);\n context.setProp(context.global, \"__requestUrl__\", urlHandle);\n urlHandle.dispose();\n\n const requestResult = context.evalCode(`new Request(__requestUrl__)`);\n\n // Clean up temporary global\n context.setProp(context.global, \"__requestUrl__\", context.undefined);\n\n if (requestResult.error) {\n const error = context.dump(requestResult.error);\n requestResult.error.dispose();\n throw new Error(`Failed to create Request: ${JSON.stringify(error)}`);\n }\n\n const requestHandle = requestResult.value;\n\n // Overwrite the internal state with our full RequestState\n setInstanceState(context, requestHandle, requestState);\n\n return requestHandle;\n}\n\n/**\n * Create the FetchHandle implementation\n */\nexport function createFetchHandle(\n context: QuickJSContext,\n stateMap: StateMap,\n serveState: ServeState\n): FetchHandle {\n const wsCommandCallbacks = new Set<(cmd: WebSocketCommand) => void>();\n\n return {\n stateMap,\n\n async dispatchRequest(request: Request): Promise<Response> {\n if (!serveState.fetchHandler) {\n throw new Error(\"No serve() handler registered\");\n }\n\n // Clear any previous upgrade request\n serveState.pendingUpgrade = null;\n\n // Convert native Request to RequestState\n const requestState = await createRequestStateFromNative(request);\n\n // Create proper QuickJS Request instance with internal state\n const requestHandle = createRequestInstance(context, stateMap, requestState);\n\n // Create Server instance using evalCode (classes must be called with 'new')\n const serverResult = context.evalCode(`new __Server__()`);\n if (serverResult.error) {\n requestHandle.dispose();\n const error = context.dump(serverResult.error);\n serverResult.error.dispose();\n throw new Error(`Failed to create Server: ${error}`);\n }\n const serverHandle = serverResult.value;\n\n try {\n // Call the fetch handler\n const result = context.callFunction(\n serveState.fetchHandler,\n context.undefined,\n requestHandle,\n serverHandle\n );\n\n if (result.error) {\n const error = context.dump(result.error);\n result.error.dispose();\n throw new Error(`Fetch handler error: ${JSON.stringify(error)}`);\n }\n\n let responseHandle = result.value;\n\n // Check if result is a Promise\n const typeofResult = context.typeof(responseHandle);\n if (typeofResult === \"object\") {\n // Check for .then method (Promise-like)\n const thenHandle = context.getProp(responseHandle, \"then\");\n const hasThen = context.typeof(thenHandle) === \"function\";\n thenHandle.dispose();\n\n if (hasThen) {\n // Resolve the promise\n const resolved = await context.resolvePromise(responseHandle);\n responseHandle.dispose();\n context.runtime.executePendingJobs();\n\n if (resolved.error) {\n const error = context.dump(resolved.error);\n resolved.error.dispose();\n throw new Error(`Fetch handler promise rejected: ${JSON.stringify(error)}`);\n }\n responseHandle = resolved.value;\n }\n }\n\n // Get the ResponseState directly from the instance\n const responseState = getInstanceState<ResponseState>(context, responseHandle);\n responseHandle.dispose();\n\n if (!responseState) {\n throw new Error(\"Failed to get Response state\");\n }\n\n // Convert to native Response\n return responseStateToNative(responseState);\n } finally {\n requestHandle.dispose();\n serverHandle.dispose();\n }\n },\n\n getUpgradeRequest(): UpgradeRequest | null {\n return serveState.pendingUpgrade;\n },\n\n /**\n * Dispatch WebSocket open event to the QuickJS handler.\n *\n * **IMPORTANT:** An `open` handler MUST be defined in the serve() websocket\n * options for the connection to be tracked. Without an open handler, the\n * connection is not stored and subsequent message/close/error events will\n * be silently ignored.\n *\n * @param connectionId - Unique identifier for this WebSocket connection (host-assigned)\n * @param data - Optional user data from server.upgrade() to associate with the connection\n */\n dispatchWebSocketOpen(connectionId: string, data?: unknown): void {\n if (!serveState.websocketHandlers.open) {\n return;\n }\n\n // Create ServerWebSocket instance using evalCode (classes must be called with 'new')\n // Pass arguments through temporary globals\n const connectionIdHandle = context.newString(connectionId);\n const dataHandle = marshal(context, data);\n context.setProp(context.global, \"__wsConnectionId__\", connectionIdHandle);\n context.setProp(context.global, \"__wsData__\", dataHandle);\n connectionIdHandle.dispose();\n dataHandle.dispose();\n\n const wsResult = context.evalCode(`new __ServerWebSocket__(__wsConnectionId__, __wsData__)`);\n\n // Clean up temporary globals\n context.setProp(context.global, \"__wsConnectionId__\", context.undefined);\n context.setProp(context.global, \"__wsData__\", context.undefined);\n\n if (wsResult.error) {\n wsResult.error.dispose();\n return;\n }\n\n const wsHandle = wsResult.value;\n\n // Store the connection\n serveState.activeConnections.set(connectionId, {\n data,\n readyState: 1,\n connectionId,\n wsHandle,\n });\n\n // Call the open handler\n const result = context.callFunction(\n serveState.websocketHandlers.open,\n context.undefined,\n wsHandle\n );\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket message event to the QuickJS handler.\n *\n * Requires the connection to have been tracked via dispatchWebSocketOpen().\n * If no message handler is defined, or the connection is not tracked, this is a no-op.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param message - The message content (string or binary data)\n */\n dispatchWebSocketMessage(\n connectionId: string,\n message: string | ArrayBuffer\n ): void {\n if (!serveState.websocketHandlers.message) {\n return;\n }\n\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n return;\n }\n\n const messageHandle =\n typeof message === \"string\"\n ? context.newString(message)\n : context.newArrayBuffer(message);\n\n const result = context.callFunction(\n serveState.websocketHandlers.message,\n context.undefined,\n connection.wsHandle,\n messageHandle\n );\n\n messageHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket close event to the QuickJS handler and clean up the connection.\n *\n * Updates the connection ready state to CLOSED (3), calls the close handler if defined,\n * disposes the WebSocket handle, and removes the connection from tracking.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param code - The WebSocket close code (e.g., 1000 for normal closure)\n * @param reason - The close reason string\n */\n dispatchWebSocketClose(\n connectionId: string,\n code: number,\n reason: string\n ): void {\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n serveState.activeConnections.delete(connectionId);\n return;\n }\n\n if (!serveState.websocketHandlers.close) {\n // No close handler, but still need to cleanup the connection\n connection.wsHandle.dispose();\n serveState.activeConnections.delete(connectionId);\n return;\n }\n\n // Update ready state\n connection.readyState = 3; // CLOSED\n\n const codeHandle = context.newNumber(code);\n const reasonHandle = context.newString(reason);\n\n const result = context.callFunction(\n serveState.websocketHandlers.close,\n context.undefined,\n connection.wsHandle,\n codeHandle,\n reasonHandle\n );\n\n codeHandle.dispose();\n reasonHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n // Cleanup\n connection.wsHandle.dispose();\n serveState.activeConnections.delete(connectionId);\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket error event to the QuickJS handler.\n *\n * Requires the connection to have been tracked via dispatchWebSocketOpen().\n * If no error handler is defined, or the connection is not tracked, this is a no-op.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param error - The error that occurred\n */\n dispatchWebSocketError(connectionId: string, error: Error): void {\n if (!serveState.websocketHandlers.error) {\n return;\n }\n\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n return;\n }\n\n const errorHandle = marshal(context, {\n name: error.name,\n message: error.message,\n });\n\n const result = context.callFunction(\n serveState.websocketHandlers.error,\n context.undefined,\n connection.wsHandle,\n errorHandle\n );\n\n errorHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Register a callback for outgoing WebSocket commands from QuickJS.\n *\n * Called when QuickJS code calls ws.send() or ws.close() on a ServerWebSocket.\n * The callback receives command objects that should be forwarded to the actual WebSocket.\n *\n * @param callback - Function to handle outgoing WebSocket commands\n * @returns Unsubscribe function to remove the callback\n */\n onWebSocketCommand(\n callback: (command: WebSocketCommand) => void\n ): () => void {\n wsCommandCallbacks.add(callback);\n return () => wsCommandCallbacks.delete(callback);\n },\n\n hasServeHandler(): boolean {\n return serveState.fetchHandler !== null;\n },\n\n dispose(): void {\n // Dispose WebSocket handler handles\n if (serveState.websocketHandlers.open) {\n serveState.websocketHandlers.open.dispose();\n serveState.websocketHandlers.open = undefined;\n }\n if (serveState.websocketHandlers.message) {\n serveState.websocketHandlers.message.dispose();\n serveState.websocketHandlers.message = undefined;\n }\n if (serveState.websocketHandlers.close) {\n serveState.websocketHandlers.close.dispose();\n serveState.websocketHandlers.close = undefined;\n }\n if (serveState.websocketHandlers.error) {\n serveState.websocketHandlers.error.dispose();\n serveState.websocketHandlers.error = undefined;\n }\n\n // Dispose fetch handler handle\n if (serveState.fetchHandler) {\n serveState.fetchHandler.dispose();\n serveState.fetchHandler = null;\n }\n\n // Dispose active WebSocket connection handles\n for (const connection of serveState.activeConnections.values()) {\n if (connection.wsHandle) {\n connection.wsHandle.dispose();\n }\n }\n serveState.activeConnections.clear();\n\n // Note: __Server__ and __ServerWebSocket__ are on global and will be cleaned up by context.dispose()\n },\n };\n}\n"
|
|
5
|
+
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport { marshal, getInstanceState, setInstanceState } from \"@ricsam/quickjs-core\";\nimport type {\n StateMap,\n FetchHandle,\n ServeState,\n UpgradeRequest,\n WebSocketCommand,\n ServerWebSocketState,\n RequestState,\n ResponseState,\n} from \"./types.cjs\";\nimport { createRequestStateFromNative } from \"./globals/request.cjs\";\nimport { responseStateToNative } from \"./globals/response.cjs\";\n\n/**\n * Create a proper Request instance in QuickJS with the given state\n */\nfunction createRequestInstance(\n context: QuickJSContext,\n stateMap: StateMap,\n requestState: RequestState\n): QuickJSHandle {\n // Create the Request instance using evalCode (classes must be called with 'new')\n // Pass the URL as the constructor argument - minimal construction\n const urlHandle = context.newString(requestState.url);\n context.setProp(context.global, \"__requestUrl__\", urlHandle);\n urlHandle.dispose();\n\n const requestResult = context.evalCode(`new Request(__requestUrl__)`);\n\n // Clean up temporary global\n context.setProp(context.global, \"__requestUrl__\", context.undefined);\n\n if (requestResult.error) {\n const error = context.dump(requestResult.error);\n requestResult.error.dispose();\n throw new Error(`Failed to create Request: ${JSON.stringify(error)}`);\n }\n\n const requestHandle = requestResult.value;\n\n // Overwrite the internal state with our full RequestState\n setInstanceState(context, requestHandle, requestState);\n\n return requestHandle;\n}\n\n/**\n * Create the FetchHandle implementation\n */\nexport function createFetchHandle(\n context: QuickJSContext,\n stateMap: StateMap,\n serveState: ServeState\n): FetchHandle {\n const wsCommandCallbacks = new Set<(cmd: WebSocketCommand) => void>();\n\n return {\n stateMap,\n\n async dispatchRequest(request: Request): Promise<Response> {\n if (!serveState.fetchHandler) {\n throw new Error(\"No serve() handler registered\");\n }\n\n // Clear any previous upgrade request\n serveState.pendingUpgrade = null;\n\n // Convert native Request to RequestState\n const requestState = await createRequestStateFromNative(request);\n\n // Create proper QuickJS Request instance with internal state\n const requestHandle = createRequestInstance(context, stateMap, requestState);\n\n // Create Server instance using evalCode (classes must be called with 'new')\n const serverResult = context.evalCode(`new __Server__()`);\n if (serverResult.error) {\n requestHandle.dispose();\n const error = context.dump(serverResult.error);\n serverResult.error.dispose();\n throw new Error(`Failed to create Server: ${error}`);\n }\n const serverHandle = serverResult.value;\n\n try {\n // Call the fetch handler\n const result = context.callFunction(\n serveState.fetchHandler,\n context.undefined,\n requestHandle,\n serverHandle\n );\n\n if (result.error) {\n const error = context.dump(result.error);\n result.error.dispose();\n throw new Error(`Fetch handler error: ${JSON.stringify(error)}`);\n }\n\n let responseHandle = result.value;\n\n // Check if result is a Promise\n const typeofResult = context.typeof(responseHandle);\n if (typeofResult === \"object\") {\n // Check for .then method (Promise-like)\n const thenHandle = context.getProp(responseHandle, \"then\");\n const hasThen = context.typeof(thenHandle) === \"function\";\n thenHandle.dispose();\n\n if (hasThen) {\n // Execute pending jobs BEFORE resolvePromise to allow\n // JavaScript-native async functions to schedule their resolution.\n // This is critical for async functions that don't use host-side awaits.\n context.runtime.executePendingJobs();\n\n // For pure JS async functions (without host-side awaits), we need to\n // wrap resolvePromise in a race with repeated executePendingJobs calls.\n // The sync variant of QuickJS needs this to process microtasks.\n const resolved = await Promise.race([\n context.resolvePromise(responseHandle),\n (async () => {\n // Keep executing pending jobs until the promise resolves\n // This handles pure JS async functions that don't trigger host callbacks\n for (let i = 0; i < 1000; i++) {\n await new Promise((r) => setTimeout(r, 0));\n context.runtime.executePendingJobs();\n }\n // If we get here, something is very wrong\n throw new Error(\"Promise resolution timeout\");\n })(),\n ]);\n responseHandle.dispose();\n context.runtime.executePendingJobs();\n\n if (resolved.error) {\n const error = context.dump(resolved.error);\n resolved.error.dispose();\n throw new Error(`Fetch handler promise rejected: ${JSON.stringify(error)}`);\n }\n responseHandle = resolved.value;\n }\n }\n\n // Get the ResponseState directly from the instance\n const responseState = getInstanceState<ResponseState>(context, responseHandle);\n responseHandle.dispose();\n\n if (!responseState) {\n throw new Error(\"Failed to get Response state\");\n }\n\n // Convert to native Response\n return responseStateToNative(responseState);\n } finally {\n requestHandle.dispose();\n serverHandle.dispose();\n }\n },\n\n getUpgradeRequest(): UpgradeRequest | null {\n return serveState.pendingUpgrade;\n },\n\n /**\n * Dispatch WebSocket open event to the QuickJS handler.\n *\n * **IMPORTANT:** An `open` handler MUST be defined in the serve() websocket\n * options for the connection to be tracked. Without an open handler, the\n * connection is not stored and subsequent message/close/error events will\n * be silently ignored.\n *\n * @param connectionId - Unique identifier for this WebSocket connection (host-assigned)\n * @param data - Optional user data from server.upgrade() to associate with the connection\n */\n dispatchWebSocketOpen(connectionId: string, data?: unknown): void {\n if (!serveState.websocketHandlers.open) {\n return;\n }\n\n // Create ServerWebSocket instance using evalCode (classes must be called with 'new')\n // Pass arguments through temporary globals\n const connectionIdHandle = context.newString(connectionId);\n const dataHandle = marshal(context, data);\n context.setProp(context.global, \"__wsConnectionId__\", connectionIdHandle);\n context.setProp(context.global, \"__wsData__\", dataHandle);\n connectionIdHandle.dispose();\n dataHandle.dispose();\n\n const wsResult = context.evalCode(`new __ServerWebSocket__(__wsConnectionId__, __wsData__)`);\n\n // Clean up temporary globals\n context.setProp(context.global, \"__wsConnectionId__\", context.undefined);\n context.setProp(context.global, \"__wsData__\", context.undefined);\n\n if (wsResult.error) {\n wsResult.error.dispose();\n return;\n }\n\n const wsHandle = wsResult.value;\n\n // Store the connection\n serveState.activeConnections.set(connectionId, {\n data,\n readyState: 1,\n connectionId,\n wsHandle,\n });\n\n // Call the open handler\n const result = context.callFunction(\n serveState.websocketHandlers.open,\n context.undefined,\n wsHandle\n );\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket message event to the QuickJS handler.\n *\n * Requires the connection to have been tracked via dispatchWebSocketOpen().\n * If no message handler is defined, or the connection is not tracked, this is a no-op.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param message - The message content (string or binary data)\n */\n dispatchWebSocketMessage(\n connectionId: string,\n message: string | ArrayBuffer\n ): void {\n if (!serveState.websocketHandlers.message) {\n return;\n }\n\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n return;\n }\n\n const messageHandle =\n typeof message === \"string\"\n ? context.newString(message)\n : context.newArrayBuffer(message);\n\n const result = context.callFunction(\n serveState.websocketHandlers.message,\n context.undefined,\n connection.wsHandle,\n messageHandle\n );\n\n messageHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket close event to the QuickJS handler and clean up the connection.\n *\n * Updates the connection ready state to CLOSED (3), calls the close handler if defined,\n * disposes the WebSocket handle, and removes the connection from tracking.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param code - The WebSocket close code (e.g., 1000 for normal closure)\n * @param reason - The close reason string\n */\n dispatchWebSocketClose(\n connectionId: string,\n code: number,\n reason: string\n ): void {\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n serveState.activeConnections.delete(connectionId);\n return;\n }\n\n if (!serveState.websocketHandlers.close) {\n // No close handler, but still need to cleanup the connection\n connection.wsHandle.dispose();\n serveState.activeConnections.delete(connectionId);\n return;\n }\n\n // Update ready state\n connection.readyState = 3; // CLOSED\n\n const codeHandle = context.newNumber(code);\n const reasonHandle = context.newString(reason);\n\n const result = context.callFunction(\n serveState.websocketHandlers.close,\n context.undefined,\n connection.wsHandle,\n codeHandle,\n reasonHandle\n );\n\n codeHandle.dispose();\n reasonHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n // Cleanup\n connection.wsHandle.dispose();\n serveState.activeConnections.delete(connectionId);\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket error event to the QuickJS handler.\n *\n * Requires the connection to have been tracked via dispatchWebSocketOpen().\n * If no error handler is defined, or the connection is not tracked, this is a no-op.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param error - The error that occurred\n */\n dispatchWebSocketError(connectionId: string, error: Error): void {\n if (!serveState.websocketHandlers.error) {\n return;\n }\n\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n return;\n }\n\n const errorHandle = marshal(context, {\n name: error.name,\n message: error.message,\n });\n\n const result = context.callFunction(\n serveState.websocketHandlers.error,\n context.undefined,\n connection.wsHandle,\n errorHandle\n );\n\n errorHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Register a callback for outgoing WebSocket commands from QuickJS.\n *\n * Called when QuickJS code calls ws.send() or ws.close() on a ServerWebSocket.\n * The callback receives command objects that should be forwarded to the actual WebSocket.\n *\n * @param callback - Function to handle outgoing WebSocket commands\n * @returns Unsubscribe function to remove the callback\n */\n onWebSocketCommand(\n callback: (command: WebSocketCommand) => void\n ): () => void {\n wsCommandCallbacks.add(callback);\n return () => wsCommandCallbacks.delete(callback);\n },\n\n hasServeHandler(): boolean {\n return serveState.fetchHandler !== null;\n },\n\n dispose(): void {\n // Dispose WebSocket handler handles\n if (serveState.websocketHandlers.open) {\n serveState.websocketHandlers.open.dispose();\n serveState.websocketHandlers.open = undefined;\n }\n if (serveState.websocketHandlers.message) {\n serveState.websocketHandlers.message.dispose();\n serveState.websocketHandlers.message = undefined;\n }\n if (serveState.websocketHandlers.close) {\n serveState.websocketHandlers.close.dispose();\n serveState.websocketHandlers.close = undefined;\n }\n if (serveState.websocketHandlers.error) {\n serveState.websocketHandlers.error.dispose();\n serveState.websocketHandlers.error = undefined;\n }\n\n // Dispose fetch handler handle\n if (serveState.fetchHandler) {\n serveState.fetchHandler.dispose();\n serveState.fetchHandler = null;\n }\n\n // Dispose active WebSocket connection handles\n for (const connection of serveState.activeConnections.values()) {\n if (connection.wsHandle) {\n connection.wsHandle.dispose();\n }\n }\n serveState.activeConnections.clear();\n\n // Note: __Server__ and __ServerWebSocket__ are on global and will be cleaned up by context.dispose()\n },\n };\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAC4D,IAA5D;AAW6C,IAA7C;AACsC,IAAtC;AAKA,SAAS,qBAAqB,CAC5B,SACA,UACA,cACe;AAAA,EAGf,MAAM,YAAY,QAAQ,UAAU,aAAa,GAAG;AAAA,EACpD,QAAQ,QAAQ,QAAQ,QAAQ,kBAAkB,SAAS;AAAA,EAC3D,UAAU,QAAQ;AAAA,EAElB,MAAM,gBAAgB,QAAQ,SAAS,6BAA6B;AAAA,EAGpE,QAAQ,QAAQ,QAAQ,QAAQ,kBAAkB,QAAQ,SAAS;AAAA,EAEnE,IAAI,cAAc,OAAO;AAAA,IACvB,MAAM,QAAQ,QAAQ,KAAK,cAAc,KAAK;AAAA,IAC9C,cAAc,MAAM,QAAQ;AAAA,IAC5B,MAAM,IAAI,MAAM,6BAA6B,KAAK,UAAU,KAAK,GAAG;AAAA,EACtE;AAAA,EAEA,MAAM,gBAAgB,cAAc;AAAA,EAGpC,qCAAiB,SAAS,eAAe,YAAY;AAAA,EAErD,OAAO;AAAA;AAMF,SAAS,iBAAiB,CAC/B,SACA,UACA,YACa;AAAA,EACb,MAAM,qBAAqB,IAAI;AAAA,EAE/B,OAAO;AAAA,IACL;AAAA,SAEM,gBAAe,CAAC,SAAqC;AAAA,MACzD,IAAI,CAAC,WAAW,cAAc;AAAA,QAC5B,MAAM,IAAI,MAAM,+BAA+B;AAAA,MACjD;AAAA,MAGA,WAAW,iBAAiB;AAAA,MAG5B,MAAM,eAAe,MAAM,4CAA6B,OAAO;AAAA,MAG/D,MAAM,gBAAgB,sBAAsB,SAAS,UAAU,YAAY;AAAA,MAG3E,MAAM,eAAe,QAAQ,SAAS,kBAAkB;AAAA,MACxD,IAAI,aAAa,OAAO;AAAA,QACtB,cAAc,QAAQ;AAAA,QACtB,MAAM,QAAQ,QAAQ,KAAK,aAAa,KAAK;AAAA,QAC7C,aAAa,MAAM,QAAQ;AAAA,QAC3B,MAAM,IAAI,MAAM,4BAA4B,OAAO;AAAA,MACrD;AAAA,MACA,MAAM,eAAe,aAAa;AAAA,MAElC,IAAI;AAAA,QAEF,MAAM,SAAS,QAAQ,aACrB,WAAW,cACX,QAAQ,WACR,eACA,YACF;AAAA,QAEA,IAAI,OAAO,OAAO;AAAA,UAChB,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK;AAAA,UACvC,OAAO,MAAM,QAAQ;AAAA,UACrB,MAAM,IAAI,MAAM,wBAAwB,KAAK,UAAU,KAAK,GAAG;AAAA,QACjE;AAAA,QAEA,IAAI,iBAAiB,OAAO;AAAA,QAG5B,MAAM,eAAe,QAAQ,OAAO,cAAc;AAAA,QAClD,IAAI,iBAAiB,UAAU;AAAA,UAE7B,MAAM,aAAa,QAAQ,QAAQ,gBAAgB,MAAM;AAAA,UACzD,MAAM,UAAU,QAAQ,OAAO,UAAU,MAAM;AAAA,UAC/C,WAAW,QAAQ;AAAA,UAEnB,IAAI,SAAS;AAAA,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "CF7F460488EA084964756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/cjs/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/abort-controller.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext } from \"quickjs-emscripten\";\n\n/**\n * Setup AbortController and AbortSignal classes in QuickJS.\n * Uses pure QuickJS implementation so that:\n * - controller.signal returns an actual AbortSignal instance\n * - Event listeners work correctly\n * - All prototype methods are accessible\n */\nexport function setupAbortControllerAndSignal(context: QuickJSContext): void {\n // Track signals for timeout feature - we need to call back into QuickJS after a delay\n const pendingTimeouts = new Map<number, ReturnType<typeof setTimeout>>();\n let nextTimeoutId = 1;\n\n // Create a host function that schedules a timeout and returns an ID\n const scheduleTimeoutFn = context.newFunction(\"__scheduleTimeout__\", (msHandle) => {\n const ms = context.getNumber(msHandle);\n const timeoutId = nextTimeoutId++;\n\n // Schedule the timeout - we'll call back into QuickJS using a polling mechanism\n const hostTimeoutId = setTimeout(() => {\n pendingTimeouts.delete(timeoutId);\n }, ms);\n\n pendingTimeouts.set(timeoutId, hostTimeoutId);\n return context.newNumber(timeoutId);\n });\n\n // Create a host function to check if a timeout has elapsed\n const checkTimeoutFn = context.newFunction(\"__checkTimeout__\", (timeoutIdHandle) => {\n const timeoutId = context.getNumber(timeoutIdHandle);\n // If the timeout is no longer in pendingTimeouts, it has elapsed\n const isElapsed = !pendingTimeouts.has(timeoutId);\n return isElapsed ? context.true : context.false;\n });\n\n context.setProp(context.global, \"__scheduleTimeout__\", scheduleTimeoutFn);\n context.setProp(context.global, \"__checkTimeout__\", checkTimeoutFn);\n scheduleTimeoutFn.dispose();\n checkTimeoutFn.dispose();\n\n // Use WeakMap for private state (more compatible than private fields)\n // Note: AbortSignal.timeout() uses a polling approach since we can't easily\n // call back into QuickJS from a host setTimeout callback\n const result = context.evalCode(`\n (function() {\n const signalState = new WeakMap();\n\n class AbortSignal {\n constructor() {\n signalState.set(this, {\n aborted: false,\n reason: undefined,\n listeners: []\n });\n }\n\n get aborted() {\n return signalState.get(this).aborted;\n }\n\n get reason() {\n return signalState.get(this).reason;\n }\n\n throwIfAborted() {\n const state = signalState.get(this);\n if (state.aborted) {\n throw state.reason ?? new DOMException(\"signal is aborted\", \"AbortError\");\n }\n }\n\n addEventListener(type, callback) {\n if (type === \"abort\" && typeof callback === \"function\") {\n signalState.get(this).listeners.push(callback);\n }\n }\n\n removeEventListener(type, callback) {\n if (type === \"abort\") {\n const state = signalState.get(this);\n const idx = state.listeners.indexOf(callback);\n if (idx >= 0) state.listeners.splice(idx, 1);\n }\n }\n\n static abort(reason) {\n const signal = new AbortSignal();\n const state = signalState.get(signal);\n state.aborted = true;\n state.reason = reason ?? new DOMException(\"signal is aborted\", \"AbortError\");\n return signal;\n }\n\n static timeout(ms) {\n const signal = new AbortSignal();\n const state = signalState.get(signal);\n const timeoutId = __scheduleTimeout__(Number(ms));\n\n // Store timeout info for later checking\n state.timeoutId = timeoutId;\n state.timeoutReason = new DOMException(\"signal timed out\", \"TimeoutError\");\n\n return signal;\n }\n\n // Internal method to check and trigger timeout\n __checkAndTriggerTimeout__() {\n const state = signalState.get(this);\n if (state.aborted || state.timeoutId === undefined) {\n return false;\n }\n\n if (__checkTimeout__(state.timeoutId)) {\n state.aborted = true;\n state.reason = state.timeoutReason;\n delete state.timeoutId;\n delete state.timeoutReason;\n\n for (const cb of state.listeners) {\n try { cb(); } catch {}\n }\n return true;\n }\n return false;\n }\n\n static any(signals) {\n const signal = new AbortSignal();\n const state = signalState.get(signal);\n\n if (!Array.isArray(signals)) return signal;\n\n for (const s of signals) {\n if (s && typeof s === \"object\" && \"aborted\" in s) {\n // Check timeout signals\n if (s.__checkAndTriggerTimeout__) {\n s.__checkAndTriggerTimeout__();\n }\n\n if (s.aborted) {\n state.aborted = true;\n state.reason = s.reason;\n return signal;\n }\n s.addEventListener(\"abort\", () => {\n if (!state.aborted) {\n state.aborted = true;\n state.reason = s.reason;\n for (const cb of state.listeners) {\n try { cb(); } catch {}\n }\n }\n });\n }\n }\n return signal;\n }\n }\n\n const controllerState = new WeakMap();\n\n class AbortController {\n constructor() {\n const signal = new AbortSignal();\n controllerState.set(this, { signal });\n }\n\n get signal() {\n return controllerState.get(this).signal;\n }\n\n abort(reason) {\n const signal = controllerState.get(this).signal;\n const state = signalState.get(signal);\n if (state.aborted) return;\n\n state.aborted = true;\n state.reason = reason ?? new DOMException(\"signal is aborted\", \"AbortError\");\n\n for (const cb of state.listeners) {\n try { cb(); } catch {}\n }\n }\n }\n\n globalThis.AbortSignal = AbortSignal;\n globalThis.AbortController = AbortController;\n })();\n `);\n\n if (result.error) {\n const error = context.dump(result.error);\n result.error.dispose();\n throw new Error(`Failed to setup AbortController: ${JSON.stringify(error)}`);\n }\n result.value.dispose();\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/fetch.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport { unmarshal, marshal } from \"@ricsam/quickjs-core\";\nimport type { RequestState, HeadersState } from \"../types.mjs\";\nimport { headersStateToNative } from \"./headers.mjs\";\nimport { createResponseStateFromNative } from \"./response.mjs\";\n\n/**\n * Create the fetch function for QuickJS\n */\nexport function createFetchFunction(\n context: QuickJSContext,\n onFetch?: (request: Request) => Promise<Response>\n): QuickJSHandle {\n return context.newFunction(\"fetch\", (...argHandles) => {\n // Create promise first\n const deferred = context.newPromise();\n\n const inputHandle = argHandles[0];\n const initHandle = argHandles[1];\n\n // Extract signal BEFORE unmarshalling (keep as handle)\n let signalHandle: QuickJSHandle | null = null;\n // Check if initHandle exists and is a valid QuickJS handle (not JS undefined)\n if (initHandle !== undefined) {\n const initType = context.typeof(initHandle);\n if (initType === \"object\") {\n const tempSignal = context.getProp(initHandle, \"signal\");\n const signalType = context.typeof(tempSignal);\n if (signalType !== \"undefined\" && signalType !== \"null\") {\n signalHandle = tempSignal;\n } else {\n tempSignal.dispose();\n }\n }\n }\n\n // Check if signal is pre-aborted\n let isPreAborted = false;\n let abortReason: { message?: string } | undefined;\n if (signalHandle) {\n const abortedHandle = context.getProp(signalHandle, \"aborted\");\n isPreAborted = Boolean(context.dump(abortedHandle));\n abortedHandle.dispose();\n\n if (isPreAborted) {\n const reasonHandle = context.getProp(signalHandle, \"reason\");\n abortReason = context.dump(reasonHandle) as { message?: string } | undefined;\n reasonHandle.dispose();\n signalHandle.dispose();\n signalHandle = null;\n }\n }\n\n // If pre-aborted, reject immediately in a microtask\n if (isPreAborted) {\n Promise.resolve().then(() => {\n // Create DOMException and reject\n const errorMessage = abortReason?.message || \"signal is aborted without reason\";\n // Escape quotes in the message for safety\n const safeMessage = errorMessage.replace(/\"/g, '\\\\\"');\n const errorResult = context.evalCode(\n `new DOMException(\"${safeMessage}\", \"AbortError\")`\n );\n if (\"value\" in errorResult) {\n deferred.reject(errorResult.value);\n errorResult.value.dispose();\n } else {\n // If evalCode failed, create a plain error\n const fallbackError = marshal(context, { name: \"AbortError\", message: errorMessage });\n deferred.reject(fallbackError);\n fallbackError.dispose();\n errorResult.error.dispose();\n }\n context.runtime.executePendingJobs();\n });\n return deferred.handle;\n }\n\n // Create native AbortController\n const nativeController = new AbortController();\n\n // Wire QuickJS signal to native controller\n if (signalHandle) {\n // Create callback that aborts native controller when QuickJS signal aborts\n const abortCallback = context.newFunction(\"__fetchAbortCallback__\", () => {\n nativeController.abort();\n return context.undefined;\n });\n\n // Call addEventListener on the QuickJS signal\n const addEventListenerHandle = context.getProp(signalHandle, \"addEventListener\");\n const typeHandle = context.newString(\"abort\");\n\n context.callFunction(addEventListenerHandle, signalHandle, typeHandle, abortCallback);\n\n typeHandle.dispose();\n addEventListenerHandle.dispose();\n abortCallback.dispose();\n signalHandle.dispose();\n }\n\n // Unmarshal arguments\n const input = inputHandle ? unmarshal(context, inputHandle) : undefined;\n const init = initHandle ? unmarshal(context, initHandle) as {\n method?: string;\n headers?: object;\n body?: unknown;\n } | undefined : undefined;\n\n // Build the request\n let url = \"\";\n let method = \"GET\";\n let headers = new Headers();\n let body: BodyInit | null = null;\n\n if (typeof input === \"string\") {\n url = input;\n } else if (input && typeof input === \"object\" && \"url\" in input) {\n // Request-like object from QuickJS\n const reqState = input as RequestState;\n url = reqState.url;\n method = reqState.method;\n headers = headersStateToNative(reqState.headersState);\n if (reqState.body) {\n body = reqState.body as BodyInit;\n }\n }\n\n // Apply init overrides\n if (init) {\n if (init.method) {\n method = init.method;\n }\n if (init.headers) {\n if (\n init.headers &&\n typeof init.headers === \"object\" &&\n \"headers\" in init.headers\n ) {\n headers = headersStateToNative(init.headers as HeadersState);\n } else {\n headers = new Headers();\n for (const [key, value] of Object.entries(init.headers)) {\n headers.set(key, String(value));\n }\n }\n }\n if (init.body !== undefined) {\n if (typeof init.body === \"string\") {\n body = init.body;\n } else if (init.body instanceof ArrayBuffer) {\n body = init.body;\n } else if (init.body instanceof Uint8Array) {\n body = init.body as BodyInit;\n } else if (\n init.body &&\n typeof init.body === \"object\" &&\n \"parts\" in init.body\n ) {\n // Blob-like\n const parts = (init.body as { parts: Uint8Array[] }).parts;\n const totalLength = parts.reduce((sum, p) => sum + p.length, 0);\n const combined = new Uint8Array(totalLength);\n let offset = 0;\n for (const part of parts) {\n combined.set(part, offset);\n offset += part.length;\n }\n body = combined as BodyInit;\n }\n }\n }\n\n // Run the async fetch operation in a microtask to ensure proper promise settling\n Promise.resolve().then(async () => {\n try {\n if (!onFetch) {\n throw new Error(\n \"fetch is not configured - no onFetch handler provided to setupFetch\"\n );\n }\n\n // Create native Request with signal\n const nativeRequest = new Request(url, {\n method,\n headers,\n body: method !== \"GET\" && method !== \"HEAD\" ? body : undefined,\n signal: nativeController.signal,\n });\n\n // Call the host fetch handler\n const nativeResponse = await onFetch(nativeRequest);\n\n // Convert to ResponseState for QuickJS\n const responseState = await createResponseStateFromNative(nativeResponse);\n const resultHandle = marshal(context, responseState);\n deferred.resolve(resultHandle);\n resultHandle.dispose();\n } catch (error) {\n // Handle abort errors specially\n if (error instanceof DOMException && error.name === \"AbortError\") {\n const safeMessage = error.message.replace(/\"/g, '\\\\\"');\n const errorResult = context.evalCode(\n `new DOMException(\"${safeMessage}\", \"AbortError\")`\n );\n if (\"value\" in errorResult) {\n deferred.reject(errorResult.value);\n errorResult.value.dispose();\n } else {\n const fallbackError = marshal(context, { name: \"AbortError\", message: error.message });\n deferred.reject(fallbackError);\n fallbackError.dispose();\n errorResult.error.dispose();\n }\n } else {\n const errorHandle = marshal(context, {\n name: error instanceof Error ? error.name : \"Error\",\n message: error instanceof Error ? error.message : String(error),\n });\n deferred.reject(errorHandle);\n errorHandle.dispose();\n }\n }\n context.runtime.executePendingJobs();\n });\n\n return deferred.handle;\n });\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/form-data.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\n\n/**\n * Internal state for FormData entries\n * Each entry can have multiple values (same key appended multiple times)\n */\nexport interface FormDataEntry {\n name: string;\n value: string | FormDataFileValue;\n}\n\nexport interface FormDataFileValue {\n data: Uint8Array;\n filename: string;\n type: string;\n}\n\nexport interface FormDataState {\n entries: FormDataEntry[];\n}\n\nfunction isFileValue(value: unknown): value is FormDataFileValue {\n return (\n value !== null &&\n typeof value === \"object\" &&\n \"data\" in value &&\n \"filename\" in value\n );\n}\n\n/**\n * Create the FormData class for QuickJS\n */\nexport function createFormDataClass(\n context: QuickJSContext,\n stateMap: StateMap\n): QuickJSHandle {\n return defineClass<FormDataState>(context, stateMap, {\n name: \"FormData\",\n construct: () => {\n return { entries: [] };\n },\n methods: {\n append(this: FormDataState, name: unknown, value: unknown, filename?: unknown) {\n const nameStr = String(name);\n\n if (value && typeof value === \"object\" && \"parts\" in value) {\n // Blob-like value\n const blobLike = value as { parts: Uint8Array[]; type?: string; name?: string };\n const data = concatenateParts(blobLike.parts);\n const fileValue: FormDataFileValue = {\n data,\n filename: filename !== undefined ? String(filename) : (blobLike.name || \"blob\"),\n type: blobLike.type || \"application/octet-stream\",\n };\n this.entries.push({ name: nameStr, value: fileValue });\n } else if (value && typeof value === \"object\" && \"data\" in value && \"filename\" in value) {\n // Already a FormDataFileValue\n this.entries.push({\n name: nameStr,\n value: {\n ...(value as FormDataFileValue),\n filename: filename !== undefined ? String(filename) : (value as FormDataFileValue).filename,\n },\n });\n } else {\n // String value\n this.entries.push({ name: nameStr, value: String(value) });\n }\n },\n delete(this: FormDataState, name: unknown) {\n const nameStr = String(name);\n this.entries = this.entries.filter((e) => e.name !== nameStr);\n },\n get(this: FormDataState, name: unknown): string | FormDataFileValue | null {\n const nameStr = String(name);\n const entry = this.entries.find((e) => e.name === nameStr);\n return entry ? entry.value : null;\n },\n getAll(this: FormDataState, name: unknown): Array<string | FormDataFileValue> {\n const nameStr = String(name);\n return this.entries\n .filter((e) => e.name === nameStr)\n .map((e) => e.value);\n },\n has(this: FormDataState, name: unknown): boolean {\n const nameStr = String(name);\n return this.entries.some((e) => e.name === nameStr);\n },\n set(this: FormDataState, name: unknown, value: unknown, filename?: unknown) {\n const nameStr = String(name);\n // Remove all existing entries with this name\n this.entries = this.entries.filter((e) => e.name !== nameStr);\n\n // Add the new entry (using append logic)\n if (value && typeof value === \"object\" && \"parts\" in value) {\n const blobLike = value as { parts: Uint8Array[]; type?: string; name?: string };\n const data = concatenateParts(blobLike.parts);\n const fileValue: FormDataFileValue = {\n data,\n filename: filename !== undefined ? String(filename) : (blobLike.name || \"blob\"),\n type: blobLike.type || \"application/octet-stream\",\n };\n this.entries.push({ name: nameStr, value: fileValue });\n } else if (value && typeof value === \"object\" && \"data\" in value && \"filename\" in value) {\n this.entries.push({\n name: nameStr,\n value: {\n ...(value as FormDataFileValue),\n filename: filename !== undefined ? String(filename) : (value as FormDataFileValue).filename,\n },\n });\n } else {\n this.entries.push({ name: nameStr, value: String(value) });\n }\n },\n entries(this: FormDataState): Array<[string, string | FormDataFileValue]> {\n return this.entries.map((e) => [e.name, e.value]);\n },\n keys(this: FormDataState): string[] {\n // Return unique keys in order of first appearance\n const seen = new Set<string>();\n const result: string[] = [];\n for (const entry of this.entries) {\n if (!seen.has(entry.name)) {\n seen.add(entry.name);\n result.push(entry.name);\n }\n }\n return result;\n },\n values(this: FormDataState): Array<string | FormDataFileValue> {\n return this.entries.map((e) => e.value);\n },\n forEach(this: FormDataState, callback: unknown) {\n if (typeof callback !== \"function\") {\n throw new TypeError(\"callback must be a function\");\n }\n for (const entry of this.entries) {\n (callback as (value: string | FormDataFileValue, key: string, parent: FormDataState) => void)(\n entry.value,\n entry.name,\n this\n );\n }\n },\n },\n });\n}\n\n/**\n * Concatenate Uint8Arrays into a single Uint8Array\n */\nfunction concatenateParts(parts: Uint8Array[]): Uint8Array {\n const totalLength = parts.reduce((sum, part) => sum + part.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const part of parts) {\n result.set(part, offset);\n offset += part.length;\n }\n return result;\n}\n\n/**\n * Parse multipart/form-data body\n */\nexport function parseMultipartFormData(\n body: Uint8Array,\n contentType: string\n): FormDataState {\n const entries: FormDataEntry[] = [];\n\n // Extract boundary from content-type\n const boundaryMatch = contentType.match(/boundary=([^;]+)/i);\n if (!boundaryMatch || !boundaryMatch[1]) {\n return { entries };\n }\n\n const boundary = boundaryMatch[1].replace(/^[\"']|[\"']$/g, \"\");\n const boundaryBytes = new TextEncoder().encode(`--${boundary}`);\n const endBoundaryBytes = new TextEncoder().encode(`--${boundary}--`);\n\n // Find all parts\n const decoder = new TextDecoder();\n let pos = 0;\n\n // Skip preamble and first boundary\n pos = findSequence(body, boundaryBytes, pos);\n if (pos === -1) return { entries };\n pos += boundaryBytes.length;\n\n while (pos < body.length) {\n // Skip CRLF after boundary\n if (body[pos] === 0x0d && body[pos + 1] === 0x0a) {\n pos += 2;\n } else if (body[pos] === 0x0a) {\n pos += 1;\n }\n\n // Check for end boundary\n if (pos + 2 <= body.length && body[pos] === 0x2d && body[pos + 1] === 0x2d) {\n break; // End of multipart\n }\n\n // Parse headers\n const headersEnd = findSequence(body, new Uint8Array([0x0d, 0x0a, 0x0d, 0x0a]), pos);\n if (headersEnd === -1) break;\n\n const headersText = decoder.decode(body.slice(pos, headersEnd));\n const headers = parseHeaders(headersText);\n pos = headersEnd + 4;\n\n // Find next boundary\n const nextBoundary = findSequence(body, boundaryBytes, pos);\n if (nextBoundary === -1) break;\n\n // Content is between current position and next boundary (minus CRLF)\n let contentEnd = nextBoundary;\n if (contentEnd > 0 && body[contentEnd - 1] === 0x0a) contentEnd--;\n if (contentEnd > 0 && body[contentEnd - 1] === 0x0d) contentEnd--;\n\n const content = body.slice(pos, contentEnd);\n\n // Parse Content-Disposition header\n const disposition = headers[\"content-disposition\"] || \"\";\n const nameMatch = disposition.match(/name=\"([^\"]+)\"/);\n const filenameMatch = disposition.match(/filename=\"([^\"]+)\"/);\n\n if (nameMatch && nameMatch[1]) {\n const name = nameMatch[1];\n if (filenameMatch && filenameMatch[1]) {\n // File entry\n const filename = filenameMatch[1];\n const type = headers[\"content-type\"] || \"application/octet-stream\";\n entries.push({\n name,\n value: { data: content, filename, type },\n });\n } else {\n // String entry\n entries.push({\n name,\n value: decoder.decode(content),\n });\n }\n }\n\n pos = nextBoundary + boundaryBytes.length;\n }\n\n return { entries };\n}\n\n/**\n * Parse URL-encoded form data\n */\nexport function parseUrlEncodedFormData(body: Uint8Array): FormDataState {\n const text = new TextDecoder().decode(body);\n const entries: FormDataEntry[] = [];\n\n const params = new URLSearchParams(text);\n for (const [name, value] of params) {\n entries.push({ name, value });\n }\n\n return { entries };\n}\n\nfunction findSequence(haystack: Uint8Array, needle: Uint8Array, start: number): number {\n outer: for (let i = start; i <= haystack.length - needle.length; i++) {\n for (let j = 0; j < needle.length; j++) {\n if (haystack[i + j] !== needle[j]) continue outer;\n }\n return i;\n }\n return -1;\n}\n\nfunction parseHeaders(text: string): Record<string, string> {\n const headers: Record<string, string> = {};\n const lines = text.split(/\\r?\\n/);\n for (const line of lines) {\n const colonIndex = line.indexOf(\":\");\n if (colonIndex > 0) {\n const name = line.slice(0, colonIndex).trim().toLowerCase();\n const value = line.slice(colonIndex + 1).trim();\n headers[name] = value;\n }\n }\n return headers;\n}\n\n/**\n * Serialize FormData to multipart/form-data format\n */\nexport function serializeFormData(state: FormDataState): { body: Uint8Array; contentType: string } {\n const boundary = `----FormDataBoundary${Math.random().toString(36).slice(2)}`;\n const encoder = new TextEncoder();\n const parts: Uint8Array[] = [];\n\n for (const entry of state.entries) {\n const headerLines: string[] = [];\n headerLines.push(`--${boundary}`);\n\n if (isFileValue(entry.value)) {\n headerLines.push(\n `Content-Disposition: form-data; name=\"${entry.name}\"; filename=\"${entry.value.filename}\"`\n );\n headerLines.push(`Content-Type: ${entry.value.type}`);\n headerLines.push(\"\");\n\n parts.push(encoder.encode(headerLines.join(\"\\r\\n\") + \"\\r\\n\"));\n parts.push(entry.value.data);\n parts.push(encoder.encode(\"\\r\\n\"));\n } else {\n headerLines.push(`Content-Disposition: form-data; name=\"${entry.name}\"`);\n headerLines.push(\"\");\n headerLines.push(entry.value);\n\n parts.push(encoder.encode(headerLines.join(\"\\r\\n\") + \"\\r\\n\"));\n }\n }\n\n parts.push(encoder.encode(`--${boundary}--\\r\\n`));\n\n // Concatenate all parts\n const totalLength = parts.reduce((sum, part) => sum + part.length, 0);\n const body = new Uint8Array(totalLength);\n let offset = 0;\n for (const part of parts) {\n body.set(part, offset);\n offset += part.length;\n }\n\n return {\n body,\n contentType: `multipart/form-data; boundary=${boundary}`,\n };\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/headers.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\nimport type { HeadersState } from \"../types.mjs\";\n\n/**\n * Create the Headers class for QuickJS\n */\nexport function createHeadersClass(\n context: QuickJSContext,\n stateMap: StateMap\n): QuickJSHandle {\n return defineClass<HeadersState>(context, stateMap, {\n name: \"Headers\",\n construct: (args) => {\n const init = args[0];\n const headers = new Map<string, string[]>();\n\n if (init) {\n if (Array.isArray(init)) {\n // Array of [key, value] pairs\n for (const pair of init) {\n if (Array.isArray(pair) && pair.length >= 2) {\n const key = String(pair[0]).toLowerCase();\n const value = String(pair[1]);\n const existing = headers.get(key) || [];\n existing.push(value);\n headers.set(key, existing);\n }\n }\n } else if (typeof init === \"object\" && init !== null) {\n // Check if it's another Headers-like object with entries method\n if (\"headers\" in init && init.headers instanceof Map) {\n // HeadersState\n for (const [key, values] of (init as HeadersState).headers) {\n headers.set(key, [...values]);\n }\n } else {\n // Plain object\n for (const [key, value] of Object.entries(init)) {\n const normalizedKey = key.toLowerCase();\n if (Array.isArray(value)) {\n headers.set(normalizedKey, value.map(String));\n } else {\n headers.set(normalizedKey, [String(value)]);\n }\n }\n }\n }\n }\n\n return { headers };\n },\n methods: {\n append(this: HeadersState, name: unknown, value: unknown) {\n const key = String(name).toLowerCase();\n const existing = this.headers.get(key) || [];\n existing.push(String(value));\n this.headers.set(key, existing);\n },\n delete(this: HeadersState, name: unknown) {\n this.headers.delete(String(name).toLowerCase());\n },\n get(this: HeadersState, name: unknown): string | null {\n const values = this.headers.get(String(name).toLowerCase());\n return values ? values.join(\", \") : null;\n },\n has(this: HeadersState, name: unknown): boolean {\n return this.headers.has(String(name).toLowerCase());\n },\n set(this: HeadersState, name: unknown, value: unknown) {\n this.headers.set(String(name).toLowerCase(), [String(value)]);\n },\n entries(this: HeadersState): Array<[string, string]> {\n const result: Array<[string, string]> = [];\n for (const [key, values] of this.headers) {\n result.push([key, values.join(\", \")]);\n }\n return result;\n },\n keys(this: HeadersState): string[] {\n return Array.from(this.headers.keys());\n },\n values(this: HeadersState): string[] {\n const result: string[] = [];\n for (const values of this.headers.values()) {\n result.push(values.join(\", \"));\n }\n return result;\n },\n forEach(this: HeadersState, callback: unknown) {\n if (typeof callback !== \"function\") {\n throw new TypeError(\"callback must be a function\");\n }\n for (const [key, values] of this.headers) {\n (callback as (value: string, key: string, parent: HeadersState) => void)(\n values.join(\", \"),\n key,\n this\n );\n }\n },\n getSetCookie(this: HeadersState): string[] {\n return this.headers.get(\"set-cookie\") || [];\n },\n },\n });\n}\n\n/**\n * Create a HeadersState from a native Headers object\n */\nexport function createHeadersStateFromNative(headers: Headers): HeadersState {\n const map = new Map<string, string[]>();\n headers.forEach((value, key) => {\n const existing = map.get(key.toLowerCase()) || [];\n existing.push(value);\n map.set(key.toLowerCase(), existing);\n });\n return { headers: map };\n}\n\n/**\n * Convert HeadersState to native Headers\n */\nexport function headersStateToNative(state: HeadersState): Headers {\n const headers = new Headers();\n for (const [key, values] of state.headers) {\n for (const value of values) {\n headers.append(key, value);\n }\n }\n return headers;\n}\n\n/**\n * Interface for a Headers-like object that can be returned from getters\n */\nexport interface HeadersLike {\n headers: Map<string, string[]>;\n append(name: string, value: string): void;\n delete(name: string): void;\n get(name: string): string | null;\n has(name: string): boolean;\n set(name: string, value: string): void;\n entries(): Array<[string, string]>;\n keys(): string[];\n values(): string[];\n forEach(callback: (value: string, key: string) => void): void;\n getSetCookie(): string[];\n}\n\n/**\n * Create a Headers-like object from HeadersState that has all the Headers methods\n * This is used for getters that need to return an object with Headers API\n */\nexport function createHeadersLike(state: HeadersState): HeadersLike {\n return {\n headers: state.headers,\n append(name: string, value: string) {\n const key = name.toLowerCase();\n const existing = state.headers.get(key) || [];\n existing.push(value);\n state.headers.set(key, existing);\n },\n delete(name: string) {\n state.headers.delete(name.toLowerCase());\n },\n get(name: string): string | null {\n const values = state.headers.get(name.toLowerCase());\n return values ? values.join(\", \") : null;\n },\n has(name: string): boolean {\n return state.headers.has(name.toLowerCase());\n },\n set(name: string, value: string) {\n state.headers.set(name.toLowerCase(), [value]);\n },\n entries(): Array<[string, string]> {\n const result: Array<[string, string]> = [];\n for (const [key, values] of state.headers) {\n result.push([key, values.join(\", \")]);\n }\n return result;\n },\n keys(): string[] {\n return Array.from(state.headers.keys());\n },\n values(): string[] {\n const result: string[] = [];\n for (const values of state.headers.values()) {\n result.push(values.join(\", \"));\n }\n return result;\n },\n forEach(callback: (value: string, key: string) => void) {\n for (const [key, values] of state.headers) {\n callback(values.join(\", \"), key);\n }\n },\n getSetCookie(): string[] {\n return state.headers.get(\"set-cookie\") || [];\n },\n };\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/request.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\nimport type { RequestState, HeadersState, AbortSignalState, FormDataState } from \"../types.mjs\";\nimport { createHeadersStateFromNative, createHeadersLike } from \"./headers.mjs\";\nimport { parseMultipartFormData, parseUrlEncodedFormData } from \"./form-data.mjs\";\n\n/**\n * Type for the stream factory function\n */\ntype StreamFactory = (source: UnderlyingSource) => QuickJSHandle;\n\n/**\n * Create the Request class for QuickJS\n */\nexport function createRequestClass(\n context: QuickJSContext,\n stateMap: StateMap,\n createStream?: StreamFactory\n): QuickJSHandle {\n return defineClass<RequestState>(context, stateMap, {\n name: \"Request\",\n construct: (args) => {\n const input = args[0];\n const init = args[1] as {\n method?: string;\n headers?: object;\n body?: unknown;\n cache?: string;\n credentials?: string;\n integrity?: string;\n keepalive?: boolean;\n mode?: string;\n redirect?: string;\n referrer?: string;\n referrerPolicy?: string;\n signal?: AbortSignalState;\n } | undefined;\n\n let url = \"\";\n let method = \"GET\";\n let headersState: HeadersState = { headers: new Map() };\n let body: Uint8Array | null = null;\n let signal: AbortSignalState | null = null;\n\n // Handle input\n if (typeof input === \"string\") {\n url = input;\n } else if (input && typeof input === \"object\") {\n // Could be URL or Request-like\n if (\"url\" in input) {\n url = String((input as { url: string }).url);\n }\n if (\"method\" in input) {\n method = String((input as { method: string }).method);\n }\n if (\"headersState\" in input) {\n const inputHeaders = (input as RequestState).headersState;\n headersState = {\n headers: new Map(inputHeaders.headers),\n };\n }\n if (\"body\" in input && (input as RequestState).body) {\n body = (input as RequestState).body;\n }\n if (\"signal\" in input) {\n signal = (input as RequestState).signal;\n }\n }\n\n // Apply init options\n if (init) {\n if (init.method) {\n method = init.method.toUpperCase();\n }\n if (init.headers) {\n if (init.headers && typeof init.headers === \"object\") {\n if (\"headers\" in init.headers && init.headers.headers instanceof Map) {\n headersState = {\n headers: new Map((init.headers as HeadersState).headers),\n };\n } else {\n headersState = { headers: new Map() };\n for (const [key, value] of Object.entries(init.headers)) {\n headersState.headers.set(key.toLowerCase(), [String(value)]);\n }\n }\n }\n }\n if (init.body !== undefined && init.body !== null) {\n if (typeof init.body === \"string\") {\n body = new TextEncoder().encode(init.body);\n } else if (init.body instanceof ArrayBuffer) {\n body = new Uint8Array(init.body);\n } else if (init.body instanceof Uint8Array) {\n body = init.body;\n }\n }\n if (init.signal) {\n signal = init.signal;\n }\n }\n\n return {\n method,\n url,\n headersState,\n body,\n bodyUsed: false,\n cache: init?.cache || \"default\",\n credentials: init?.credentials || \"same-origin\",\n destination: \"\",\n integrity: init?.integrity || \"\",\n keepalive: init?.keepalive || false,\n mode: init?.mode || \"cors\",\n redirect: init?.redirect || \"follow\",\n referrer: init?.referrer || \"about:client\",\n referrerPolicy: init?.referrerPolicy || \"\",\n signal,\n };\n },\n properties: {\n method: {\n get(this: RequestState) {\n return this.method;\n },\n },\n url: {\n get(this: RequestState) {\n return this.url;\n },\n },\n headers: {\n get(this: RequestState) {\n return createHeadersLike(this.headersState);\n },\n },\n body: {\n get(this: RequestState) {\n if (!this.body) return null;\n if (!createStream) {\n // Fallback: return raw body if no stream factory\n return this.body;\n }\n // Create a ReadableStream from the body data\n const bodyData = this.body;\n let offset = 0;\n const chunkSize = 65536; // 64KB chunks\n return createStream({\n pull(controller) {\n if (offset >= bodyData.length) {\n controller.close();\n return;\n }\n const chunk = bodyData.slice(offset, Math.min(offset + chunkSize, bodyData.length));\n offset += chunk.length;\n controller.enqueue(chunk);\n },\n });\n },\n },\n bodyUsed: {\n get(this: RequestState) {\n return this.bodyUsed;\n },\n },\n cache: {\n get(this: RequestState) {\n return this.cache;\n },\n },\n credentials: {\n get(this: RequestState) {\n return this.credentials;\n },\n },\n destination: {\n get(this: RequestState) {\n return this.destination;\n },\n },\n integrity: {\n get(this: RequestState) {\n return this.integrity;\n },\n },\n keepalive: {\n get(this: RequestState) {\n return this.keepalive;\n },\n },\n mode: {\n get(this: RequestState) {\n return this.mode;\n },\n },\n redirect: {\n get(this: RequestState) {\n return this.redirect;\n },\n },\n referrer: {\n get(this: RequestState) {\n return this.referrer;\n },\n },\n referrerPolicy: {\n get(this: RequestState) {\n return this.referrerPolicy;\n },\n },\n signal: {\n get(this: RequestState) {\n return this.signal;\n },\n },\n },\n methods: {\n async arrayBuffer(this: RequestState): Promise<ArrayBuffer> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return new ArrayBuffer(0);\n }\n return this.body.buffer.slice(\n this.body.byteOffset,\n this.body.byteOffset + this.body.byteLength\n ) as ArrayBuffer;\n },\n async blob(this: RequestState): Promise<object> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n const contentType = this.headersState.headers.get(\"content-type\")?.[0] || \"\";\n return {\n parts: this.body ? [this.body] : [],\n type: contentType,\n size: this.body?.length || 0,\n };\n },\n clone(this: RequestState): RequestState {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n return {\n ...this,\n headersState: {\n headers: new Map(this.headersState.headers),\n },\n body: this.body ? new Uint8Array(this.body) : null,\n bodyUsed: false,\n };\n },\n async json(this: RequestState): Promise<unknown> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return JSON.parse(\"\");\n }\n const text = new TextDecoder().decode(this.body);\n return JSON.parse(text);\n },\n async text(this: RequestState): Promise<string> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return \"\";\n }\n return new TextDecoder().decode(this.body);\n },\n async formData(this: RequestState): Promise<FormDataState> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return { entries: [] };\n }\n\n const contentType = this.headersState.headers.get(\"content-type\")?.[0] || \"\";\n\n if (contentType.includes(\"multipart/form-data\")) {\n return parseMultipartFormData(this.body, contentType);\n } else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n return parseUrlEncodedFormData(this.body);\n }\n\n throw new TypeError(\"Could not parse content as FormData\");\n },\n },\n });\n}\n\n/**\n * Create a RequestState from a native Request object\n */\nexport async function createRequestStateFromNative(\n request: Request\n): Promise<RequestState> {\n const body = request.body\n ? new Uint8Array(await request.arrayBuffer())\n : null;\n\n return {\n method: request.method,\n url: request.url,\n headersState: createHeadersStateFromNative(request.headers),\n body,\n bodyUsed: false,\n cache: request.cache,\n credentials: request.credentials,\n destination: request.destination,\n integrity: request.integrity,\n keepalive: request.keepalive,\n mode: request.mode,\n redirect: request.redirect,\n referrer: request.referrer,\n referrerPolicy: request.referrerPolicy,\n signal: null, // Signal handling is complex, simplified here\n };\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/response.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\nimport type { ResponseState, HeadersState, FormDataState } from \"../types.mjs\";\nimport { headersStateToNative, createHeadersLike } from \"./headers.mjs\";\nimport { parseMultipartFormData, parseUrlEncodedFormData } from \"./form-data.mjs\";\n\n/**\n * Type for the stream factory function\n */\ntype StreamFactory = (source: UnderlyingSource) => QuickJSHandle;\n\n/**\n * Create the Response class for QuickJS\n */\nexport function createResponseClass(\n context: QuickJSContext,\n stateMap: StateMap,\n createStream?: StreamFactory\n): QuickJSHandle {\n const classHandle = defineClass<ResponseState>(context, stateMap, {\n name: \"Response\",\n construct: (args) => {\n const bodyInit = args[0];\n const init = args[1] as {\n status?: number;\n statusText?: string;\n headers?: object;\n _type?: string; // Internal: for Response.error() to set type\n } | undefined;\n\n let body: Uint8Array | null = null;\n const status = init?.status ?? 200;\n const statusText = init?.statusText ?? \"\";\n let headersState: HeadersState = { headers: new Map() };\n\n // Parse body\n if (bodyInit !== null && bodyInit !== undefined) {\n if (typeof bodyInit === \"string\") {\n body = new TextEncoder().encode(bodyInit);\n } else if (bodyInit instanceof ArrayBuffer) {\n body = new Uint8Array(bodyInit);\n } else if (bodyInit instanceof Uint8Array) {\n body = bodyInit;\n } else if (\n bodyInit &&\n typeof bodyInit === \"object\" &&\n \"parts\" in bodyInit\n ) {\n // Blob-like\n const parts = (bodyInit as { parts: Uint8Array[] }).parts;\n const totalLength = parts.reduce((sum, p) => sum + p.length, 0);\n body = new Uint8Array(totalLength);\n let offset = 0;\n for (const part of parts) {\n body.set(part, offset);\n offset += part.length;\n }\n }\n }\n\n // Parse headers\n if (init?.headers) {\n if (\n init.headers &&\n typeof init.headers === \"object\" &&\n \"headers\" in init.headers &&\n init.headers.headers instanceof Map\n ) {\n headersState = {\n headers: new Map((init.headers as HeadersState).headers),\n };\n } else {\n headersState = { headers: new Map() };\n for (const [key, value] of Object.entries(init.headers)) {\n headersState.headers.set(key.toLowerCase(), [String(value)]);\n }\n }\n }\n\n return {\n status,\n statusText,\n headersState,\n body,\n bodyUsed: false,\n url: \"\",\n redirected: false,\n type: init?._type ?? \"default\",\n ok: status >= 200 && status < 300,\n };\n },\n properties: {\n body: {\n get(this: ResponseState) {\n if (!this.body) return null;\n if (!createStream) {\n // Fallback: return raw body if no stream factory\n return this.body;\n }\n // Create a ReadableStream from the body data\n const bodyData = this.body;\n let offset = 0;\n const chunkSize = 65536; // 64KB chunks\n return createStream({\n pull(controller) {\n if (offset >= bodyData.length) {\n controller.close();\n return;\n }\n const chunk = bodyData.slice(offset, Math.min(offset + chunkSize, bodyData.length));\n offset += chunk.length;\n controller.enqueue(chunk);\n },\n });\n },\n },\n bodyUsed: {\n get(this: ResponseState) {\n return this.bodyUsed;\n },\n },\n headers: {\n get(this: ResponseState) {\n return createHeadersLike(this.headersState);\n },\n },\n ok: {\n get(this: ResponseState) {\n return this.ok;\n },\n },\n redirected: {\n get(this: ResponseState) {\n return this.redirected;\n },\n },\n status: {\n get(this: ResponseState) {\n return this.status;\n },\n },\n statusText: {\n get(this: ResponseState) {\n return this.statusText;\n },\n },\n type: {\n get(this: ResponseState) {\n return this.type;\n },\n },\n url: {\n get(this: ResponseState) {\n return this.url;\n },\n },\n },\n methods: {\n async arrayBuffer(this: ResponseState): Promise<ArrayBuffer> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return new ArrayBuffer(0);\n }\n return this.body.buffer.slice(\n this.body.byteOffset,\n this.body.byteOffset + this.body.byteLength\n ) as ArrayBuffer;\n },\n async blob(this: ResponseState): Promise<object> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n const contentType =\n this.headersState.headers.get(\"content-type\")?.[0] || \"\";\n return {\n parts: this.body ? [this.body] : [],\n type: contentType,\n size: this.body?.length || 0,\n };\n },\n clone(this: ResponseState): ResponseState {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n return {\n ...this,\n headersState: {\n headers: new Map(this.headersState.headers),\n },\n body: this.body ? new Uint8Array(this.body) : null,\n bodyUsed: false,\n };\n },\n async json(this: ResponseState): Promise<unknown> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return JSON.parse(\"\");\n }\n const text = new TextDecoder().decode(this.body);\n return JSON.parse(text);\n },\n async text(this: ResponseState): Promise<string> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return \"\";\n }\n return new TextDecoder().decode(this.body);\n },\n async formData(this: ResponseState): Promise<FormDataState> {\n if (this.bodyUsed) {\n throw new TypeError(\"Body has already been consumed\");\n }\n this.bodyUsed = true;\n if (!this.body) {\n return { entries: [] };\n }\n\n const contentType = this.headersState.headers.get(\"content-type\")?.[0] || \"\";\n\n if (contentType.includes(\"multipart/form-data\")) {\n return parseMultipartFormData(this.body, contentType);\n } else if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n return parseUrlEncodedFormData(this.body);\n }\n\n throw new TypeError(\"Could not parse content as FormData\");\n },\n },\n });\n\n return classHandle;\n}\n\n/**\n * Add static methods to Response class after it's been set on global\n * This must be called after Response and Headers are available on global\n */\nexport function addResponseStaticMethods(context: QuickJSContext): void {\n const staticMethodsCode = `\n Response.error = function() {\n return new Response(null, { status: 0, _type: \"error\" });\n };\n\n Response.json = function(data, init = {}) {\n const body = JSON.stringify(data);\n // Merge content-type with any provided headers\n const headers = Object.assign(\n { \"content-type\": \"application/json\" },\n init.headers || {}\n );\n return new Response(body, {\n status: init.status ?? 200,\n statusText: init.statusText ?? \"\",\n headers: headers\n });\n };\n\n Response.redirect = function(url, status = 302) {\n return new Response(null, {\n status: status,\n headers: { \"location\": String(url) }\n });\n };\n `;\n const result = context.evalCode(staticMethodsCode);\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n}\n\n/**\n * Convert ResponseState (or unmarshalled Response object) to native Response\n */\nexport function responseStateToNative(state: ResponseState | Record<string, unknown>): Response {\n const body = (state as ResponseState).body ?? (state as Record<string, unknown>).body;\n const status = (state as ResponseState).status ?? 200;\n const statusText = (state as ResponseState).statusText ?? \"\";\n\n // Handle both headersState (internal) and headers (from getter)\n let headersState: HeadersState;\n if ((state as ResponseState).headersState) {\n headersState = (state as ResponseState).headersState;\n } else if ((state as Record<string, unknown>).headers) {\n // When unmarshalled, headers is the HeadersLike object from getter\n const headers = (state as Record<string, unknown>).headers as { headers?: Map<string, string[]> };\n if (headers.headers instanceof Map) {\n headersState = { headers: headers.headers };\n } else {\n headersState = { headers: new Map() };\n }\n } else {\n headersState = { headers: new Map() };\n }\n\n return new Response(body as BodyInit | null, {\n status,\n statusText,\n headers: headersStateToNative(headersState),\n });\n}\n\n/**\n * Create a ResponseState from a native Response\n */\nexport async function createResponseStateFromNative(\n response: Response\n): Promise<ResponseState> {\n const body = response.body\n ? new Uint8Array(await response.arrayBuffer())\n : null;\n\n const headersState: HeadersState = { headers: new Map() };\n response.headers.forEach((value, key) => {\n const existing = headersState.headers.get(key.toLowerCase()) || [];\n existing.push(value);\n headersState.headers.set(key.toLowerCase(), existing);\n });\n\n return {\n status: response.status,\n statusText: response.statusText,\n headersState,\n body,\n bodyUsed: false,\n url: response.url,\n redirected: response.redirected,\n type: response.type,\n ok: response.ok,\n };\n}\n"
|
|
6
6
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../../src/globals/serve.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport type { StateMap } from \"@ricsam/quickjs-core\";\nimport { defineClass } from \"@ricsam/quickjs-core\";\nimport type { ServeState, ServerWebSocketState, WebSocketCommand } from \"../types.mjs\";\n\ninterface ServerState {\n serveState: ServeState;\n}\n\n/**\n * Create the ServerWebSocket class for QuickJS\n */\nexport function createServerWebSocketClass(\n context: QuickJSContext,\n stateMap: StateMap,\n onWebSocketCommand: (cmd: WebSocketCommand) => void\n): QuickJSHandle {\n return defineClass<ServerWebSocketState>(context, stateMap, {\n name: \"ServerWebSocket\",\n construct: (args) => {\n const connectionId = String(args[0]);\n const data = args[1];\n\n return {\n data,\n readyState: 1, // OPEN\n connectionId,\n wsHandle: null,\n };\n },\n properties: {\n data: {\n get(this: ServerWebSocketState) {\n return this.data;\n },\n },\n readyState: {\n get(this: ServerWebSocketState) {\n return this.readyState;\n },\n },\n },\n methods: {\n send(this: ServerWebSocketState, message: unknown) {\n if (this.readyState !== 1) {\n throw new Error(\"WebSocket is not open\");\n }\n\n let data: string | ArrayBuffer;\n if (typeof message === \"string\") {\n data = message;\n } else if (message instanceof ArrayBuffer) {\n data = message;\n } else if (message instanceof Uint8Array) {\n data = message.buffer.slice(\n message.byteOffset,\n message.byteOffset + message.byteLength\n ) as ArrayBuffer;\n } else {\n data = String(message);\n }\n\n onWebSocketCommand({\n type: \"message\",\n connectionId: this.connectionId,\n data,\n });\n },\n close(this: ServerWebSocketState, code?: unknown, reason?: unknown) {\n if (this.readyState === 3) {\n return; // Already closed\n }\n\n this.readyState = 2; // CLOSING\n\n onWebSocketCommand({\n type: \"close\",\n connectionId: this.connectionId,\n code: typeof code === \"number\" ? code : undefined,\n reason: typeof reason === \"string\" ? reason : undefined,\n });\n },\n },\n });\n}\n\n/**\n * Create the Server class for QuickJS\n */\nexport function createServerClass(\n context: QuickJSContext,\n stateMap: StateMap,\n serveState: ServeState\n): QuickJSHandle {\n return defineClass<ServerState>(context, stateMap, {\n name: \"Server\",\n construct: () => {\n return { serveState };\n },\n methods: {\n upgrade(\n this: ServerState,\n _request: unknown,\n options?: unknown\n ): boolean {\n const data =\n options && typeof options === \"object\" && \"data\" in options\n ? (options as { data: unknown }).data\n : undefined;\n\n this.serveState.pendingUpgrade = {\n requested: true,\n data,\n };\n\n return true;\n },\n },\n });\n}\n\n/**\n * Create the serve function for QuickJS\n * Uses context.newFunction instead of defineFunction to properly manage handle lifetimes\n */\nexport function createServeFunction(\n context: QuickJSContext,\n stateMap: StateMap,\n serveState: ServeState\n): QuickJSHandle {\n return context.newFunction(\"serve\", (optionsHandle) => {\n if (context.typeof(optionsHandle) !== \"object\") {\n throw context.newError(\"serve requires an options object\");\n }\n\n // Dispose previous handlers if any\n if (serveState.fetchHandler) {\n serveState.fetchHandler.dispose();\n serveState.fetchHandler = null;\n }\n\n // Get and store fetch handler - don't dispose, keep alive for later calls\n const fetchHandle = context.getProp(optionsHandle, \"fetch\");\n if (context.typeof(fetchHandle) === \"function\") {\n serveState.fetchHandler = fetchHandle;\n } else {\n fetchHandle.dispose();\n }\n\n // Get and store websocket handlers\n const websocketHandle = context.getProp(optionsHandle, \"websocket\");\n if (context.typeof(websocketHandle) === \"object\") {\n // Dispose previous handlers\n if (serveState.websocketHandlers.open) {\n serveState.websocketHandlers.open.dispose();\n serveState.websocketHandlers.open = undefined;\n }\n if (serveState.websocketHandlers.message) {\n serveState.websocketHandlers.message.dispose();\n serveState.websocketHandlers.message = undefined;\n }\n if (serveState.websocketHandlers.close) {\n serveState.websocketHandlers.close.dispose();\n serveState.websocketHandlers.close = undefined;\n }\n if (serveState.websocketHandlers.error) {\n serveState.websocketHandlers.error.dispose();\n serveState.websocketHandlers.error = undefined;\n }\n\n const openHandle = context.getProp(websocketHandle, \"open\");\n if (context.typeof(openHandle) === \"function\") {\n serveState.websocketHandlers.open = openHandle;\n } else {\n openHandle.dispose();\n }\n\n const messageHandle = context.getProp(websocketHandle, \"message\");\n if (context.typeof(messageHandle) === \"function\") {\n serveState.websocketHandlers.message = messageHandle;\n } else {\n messageHandle.dispose();\n }\n\n const closeHandle = context.getProp(websocketHandle, \"close\");\n if (context.typeof(closeHandle) === \"function\") {\n serveState.websocketHandlers.close = closeHandle;\n } else {\n closeHandle.dispose();\n }\n\n const errorHandle = context.getProp(websocketHandle, \"error\");\n if (context.typeof(errorHandle) === \"function\") {\n serveState.websocketHandlers.error = errorHandle;\n } else {\n errorHandle.dispose();\n }\n }\n websocketHandle.dispose();\n\n return context.undefined;\n });\n}\n"
|
|
6
6
|
],
|
package/dist/mjs/handle.mjs
CHANGED
|
@@ -51,7 +51,17 @@ function createFetchHandle(context, stateMap, serveState) {
|
|
|
51
51
|
const hasThen = context.typeof(thenHandle) === "function";
|
|
52
52
|
thenHandle.dispose();
|
|
53
53
|
if (hasThen) {
|
|
54
|
-
|
|
54
|
+
context.runtime.executePendingJobs();
|
|
55
|
+
const resolved = await Promise.race([
|
|
56
|
+
context.resolvePromise(responseHandle),
|
|
57
|
+
(async () => {
|
|
58
|
+
for (let i = 0;i < 1000; i++) {
|
|
59
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
60
|
+
context.runtime.executePendingJobs();
|
|
61
|
+
}
|
|
62
|
+
throw new Error("Promise resolution timeout");
|
|
63
|
+
})()
|
|
64
|
+
]);
|
|
55
65
|
responseHandle.dispose();
|
|
56
66
|
context.runtime.executePendingJobs();
|
|
57
67
|
if (resolved.error) {
|
|
@@ -214,4 +224,4 @@ export {
|
|
|
214
224
|
createFetchHandle
|
|
215
225
|
};
|
|
216
226
|
|
|
217
|
-
//# debugId=
|
|
227
|
+
//# debugId=C4B0D934788B3BC064756E2164756E21
|
package/dist/mjs/handle.mjs.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/handle.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport { marshal, getInstanceState, setInstanceState } from \"@ricsam/quickjs-core\";\nimport type {\n StateMap,\n FetchHandle,\n ServeState,\n UpgradeRequest,\n WebSocketCommand,\n ServerWebSocketState,\n RequestState,\n ResponseState,\n} from \"./types.mjs\";\nimport { createRequestStateFromNative } from \"./globals/request.mjs\";\nimport { responseStateToNative } from \"./globals/response.mjs\";\n\n/**\n * Create a proper Request instance in QuickJS with the given state\n */\nfunction createRequestInstance(\n context: QuickJSContext,\n stateMap: StateMap,\n requestState: RequestState\n): QuickJSHandle {\n // Create the Request instance using evalCode (classes must be called with 'new')\n // Pass the URL as the constructor argument - minimal construction\n const urlHandle = context.newString(requestState.url);\n context.setProp(context.global, \"__requestUrl__\", urlHandle);\n urlHandle.dispose();\n\n const requestResult = context.evalCode(`new Request(__requestUrl__)`);\n\n // Clean up temporary global\n context.setProp(context.global, \"__requestUrl__\", context.undefined);\n\n if (requestResult.error) {\n const error = context.dump(requestResult.error);\n requestResult.error.dispose();\n throw new Error(`Failed to create Request: ${JSON.stringify(error)}`);\n }\n\n const requestHandle = requestResult.value;\n\n // Overwrite the internal state with our full RequestState\n setInstanceState(context, requestHandle, requestState);\n\n return requestHandle;\n}\n\n/**\n * Create the FetchHandle implementation\n */\nexport function createFetchHandle(\n context: QuickJSContext,\n stateMap: StateMap,\n serveState: ServeState\n): FetchHandle {\n const wsCommandCallbacks = new Set<(cmd: WebSocketCommand) => void>();\n\n return {\n stateMap,\n\n async dispatchRequest(request: Request): Promise<Response> {\n if (!serveState.fetchHandler) {\n throw new Error(\"No serve() handler registered\");\n }\n\n // Clear any previous upgrade request\n serveState.pendingUpgrade = null;\n\n // Convert native Request to RequestState\n const requestState = await createRequestStateFromNative(request);\n\n // Create proper QuickJS Request instance with internal state\n const requestHandle = createRequestInstance(context, stateMap, requestState);\n\n // Create Server instance using evalCode (classes must be called with 'new')\n const serverResult = context.evalCode(`new __Server__()`);\n if (serverResult.error) {\n requestHandle.dispose();\n const error = context.dump(serverResult.error);\n serverResult.error.dispose();\n throw new Error(`Failed to create Server: ${error}`);\n }\n const serverHandle = serverResult.value;\n\n try {\n // Call the fetch handler\n const result = context.callFunction(\n serveState.fetchHandler,\n context.undefined,\n requestHandle,\n serverHandle\n );\n\n if (result.error) {\n const error = context.dump(result.error);\n result.error.dispose();\n throw new Error(`Fetch handler error: ${JSON.stringify(error)}`);\n }\n\n let responseHandle = result.value;\n\n // Check if result is a Promise\n const typeofResult = context.typeof(responseHandle);\n if (typeofResult === \"object\") {\n // Check for .then method (Promise-like)\n const thenHandle = context.getProp(responseHandle, \"then\");\n const hasThen = context.typeof(thenHandle) === \"function\";\n thenHandle.dispose();\n\n if (hasThen) {\n // Resolve the promise\n const resolved = await context.resolvePromise(responseHandle);\n responseHandle.dispose();\n context.runtime.executePendingJobs();\n\n if (resolved.error) {\n const error = context.dump(resolved.error);\n resolved.error.dispose();\n throw new Error(`Fetch handler promise rejected: ${JSON.stringify(error)}`);\n }\n responseHandle = resolved.value;\n }\n }\n\n // Get the ResponseState directly from the instance\n const responseState = getInstanceState<ResponseState>(context, responseHandle);\n responseHandle.dispose();\n\n if (!responseState) {\n throw new Error(\"Failed to get Response state\");\n }\n\n // Convert to native Response\n return responseStateToNative(responseState);\n } finally {\n requestHandle.dispose();\n serverHandle.dispose();\n }\n },\n\n getUpgradeRequest(): UpgradeRequest | null {\n return serveState.pendingUpgrade;\n },\n\n /**\n * Dispatch WebSocket open event to the QuickJS handler.\n *\n * **IMPORTANT:** An `open` handler MUST be defined in the serve() websocket\n * options for the connection to be tracked. Without an open handler, the\n * connection is not stored and subsequent message/close/error events will\n * be silently ignored.\n *\n * @param connectionId - Unique identifier for this WebSocket connection (host-assigned)\n * @param data - Optional user data from server.upgrade() to associate with the connection\n */\n dispatchWebSocketOpen(connectionId: string, data?: unknown): void {\n if (!serveState.websocketHandlers.open) {\n return;\n }\n\n // Create ServerWebSocket instance using evalCode (classes must be called with 'new')\n // Pass arguments through temporary globals\n const connectionIdHandle = context.newString(connectionId);\n const dataHandle = marshal(context, data);\n context.setProp(context.global, \"__wsConnectionId__\", connectionIdHandle);\n context.setProp(context.global, \"__wsData__\", dataHandle);\n connectionIdHandle.dispose();\n dataHandle.dispose();\n\n const wsResult = context.evalCode(`new __ServerWebSocket__(__wsConnectionId__, __wsData__)`);\n\n // Clean up temporary globals\n context.setProp(context.global, \"__wsConnectionId__\", context.undefined);\n context.setProp(context.global, \"__wsData__\", context.undefined);\n\n if (wsResult.error) {\n wsResult.error.dispose();\n return;\n }\n\n const wsHandle = wsResult.value;\n\n // Store the connection\n serveState.activeConnections.set(connectionId, {\n data,\n readyState: 1,\n connectionId,\n wsHandle,\n });\n\n // Call the open handler\n const result = context.callFunction(\n serveState.websocketHandlers.open,\n context.undefined,\n wsHandle\n );\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket message event to the QuickJS handler.\n *\n * Requires the connection to have been tracked via dispatchWebSocketOpen().\n * If no message handler is defined, or the connection is not tracked, this is a no-op.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param message - The message content (string or binary data)\n */\n dispatchWebSocketMessage(\n connectionId: string,\n message: string | ArrayBuffer\n ): void {\n if (!serveState.websocketHandlers.message) {\n return;\n }\n\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n return;\n }\n\n const messageHandle =\n typeof message === \"string\"\n ? context.newString(message)\n : context.newArrayBuffer(message);\n\n const result = context.callFunction(\n serveState.websocketHandlers.message,\n context.undefined,\n connection.wsHandle,\n messageHandle\n );\n\n messageHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket close event to the QuickJS handler and clean up the connection.\n *\n * Updates the connection ready state to CLOSED (3), calls the close handler if defined,\n * disposes the WebSocket handle, and removes the connection from tracking.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param code - The WebSocket close code (e.g., 1000 for normal closure)\n * @param reason - The close reason string\n */\n dispatchWebSocketClose(\n connectionId: string,\n code: number,\n reason: string\n ): void {\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n serveState.activeConnections.delete(connectionId);\n return;\n }\n\n if (!serveState.websocketHandlers.close) {\n // No close handler, but still need to cleanup the connection\n connection.wsHandle.dispose();\n serveState.activeConnections.delete(connectionId);\n return;\n }\n\n // Update ready state\n connection.readyState = 3; // CLOSED\n\n const codeHandle = context.newNumber(code);\n const reasonHandle = context.newString(reason);\n\n const result = context.callFunction(\n serveState.websocketHandlers.close,\n context.undefined,\n connection.wsHandle,\n codeHandle,\n reasonHandle\n );\n\n codeHandle.dispose();\n reasonHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n // Cleanup\n connection.wsHandle.dispose();\n serveState.activeConnections.delete(connectionId);\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket error event to the QuickJS handler.\n *\n * Requires the connection to have been tracked via dispatchWebSocketOpen().\n * If no error handler is defined, or the connection is not tracked, this is a no-op.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param error - The error that occurred\n */\n dispatchWebSocketError(connectionId: string, error: Error): void {\n if (!serveState.websocketHandlers.error) {\n return;\n }\n\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n return;\n }\n\n const errorHandle = marshal(context, {\n name: error.name,\n message: error.message,\n });\n\n const result = context.callFunction(\n serveState.websocketHandlers.error,\n context.undefined,\n connection.wsHandle,\n errorHandle\n );\n\n errorHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Register a callback for outgoing WebSocket commands from QuickJS.\n *\n * Called when QuickJS code calls ws.send() or ws.close() on a ServerWebSocket.\n * The callback receives command objects that should be forwarded to the actual WebSocket.\n *\n * @param callback - Function to handle outgoing WebSocket commands\n * @returns Unsubscribe function to remove the callback\n */\n onWebSocketCommand(\n callback: (command: WebSocketCommand) => void\n ): () => void {\n wsCommandCallbacks.add(callback);\n return () => wsCommandCallbacks.delete(callback);\n },\n\n hasServeHandler(): boolean {\n return serveState.fetchHandler !== null;\n },\n\n dispose(): void {\n // Dispose WebSocket handler handles\n if (serveState.websocketHandlers.open) {\n serveState.websocketHandlers.open.dispose();\n serveState.websocketHandlers.open = undefined;\n }\n if (serveState.websocketHandlers.message) {\n serveState.websocketHandlers.message.dispose();\n serveState.websocketHandlers.message = undefined;\n }\n if (serveState.websocketHandlers.close) {\n serveState.websocketHandlers.close.dispose();\n serveState.websocketHandlers.close = undefined;\n }\n if (serveState.websocketHandlers.error) {\n serveState.websocketHandlers.error.dispose();\n serveState.websocketHandlers.error = undefined;\n }\n\n // Dispose fetch handler handle\n if (serveState.fetchHandler) {\n serveState.fetchHandler.dispose();\n serveState.fetchHandler = null;\n }\n\n // Dispose active WebSocket connection handles\n for (const connection of serveState.activeConnections.values()) {\n if (connection.wsHandle) {\n connection.wsHandle.dispose();\n }\n }\n serveState.activeConnections.clear();\n\n // Note: __Server__ and __ServerWebSocket__ are on global and will be cleaned up by context.dispose()\n },\n };\n}\n"
|
|
5
|
+
"import type { QuickJSContext, QuickJSHandle } from \"quickjs-emscripten\";\nimport { marshal, getInstanceState, setInstanceState } from \"@ricsam/quickjs-core\";\nimport type {\n StateMap,\n FetchHandle,\n ServeState,\n UpgradeRequest,\n WebSocketCommand,\n ServerWebSocketState,\n RequestState,\n ResponseState,\n} from \"./types.mjs\";\nimport { createRequestStateFromNative } from \"./globals/request.mjs\";\nimport { responseStateToNative } from \"./globals/response.mjs\";\n\n/**\n * Create a proper Request instance in QuickJS with the given state\n */\nfunction createRequestInstance(\n context: QuickJSContext,\n stateMap: StateMap,\n requestState: RequestState\n): QuickJSHandle {\n // Create the Request instance using evalCode (classes must be called with 'new')\n // Pass the URL as the constructor argument - minimal construction\n const urlHandle = context.newString(requestState.url);\n context.setProp(context.global, \"__requestUrl__\", urlHandle);\n urlHandle.dispose();\n\n const requestResult = context.evalCode(`new Request(__requestUrl__)`);\n\n // Clean up temporary global\n context.setProp(context.global, \"__requestUrl__\", context.undefined);\n\n if (requestResult.error) {\n const error = context.dump(requestResult.error);\n requestResult.error.dispose();\n throw new Error(`Failed to create Request: ${JSON.stringify(error)}`);\n }\n\n const requestHandle = requestResult.value;\n\n // Overwrite the internal state with our full RequestState\n setInstanceState(context, requestHandle, requestState);\n\n return requestHandle;\n}\n\n/**\n * Create the FetchHandle implementation\n */\nexport function createFetchHandle(\n context: QuickJSContext,\n stateMap: StateMap,\n serveState: ServeState\n): FetchHandle {\n const wsCommandCallbacks = new Set<(cmd: WebSocketCommand) => void>();\n\n return {\n stateMap,\n\n async dispatchRequest(request: Request): Promise<Response> {\n if (!serveState.fetchHandler) {\n throw new Error(\"No serve() handler registered\");\n }\n\n // Clear any previous upgrade request\n serveState.pendingUpgrade = null;\n\n // Convert native Request to RequestState\n const requestState = await createRequestStateFromNative(request);\n\n // Create proper QuickJS Request instance with internal state\n const requestHandle = createRequestInstance(context, stateMap, requestState);\n\n // Create Server instance using evalCode (classes must be called with 'new')\n const serverResult = context.evalCode(`new __Server__()`);\n if (serverResult.error) {\n requestHandle.dispose();\n const error = context.dump(serverResult.error);\n serverResult.error.dispose();\n throw new Error(`Failed to create Server: ${error}`);\n }\n const serverHandle = serverResult.value;\n\n try {\n // Call the fetch handler\n const result = context.callFunction(\n serveState.fetchHandler,\n context.undefined,\n requestHandle,\n serverHandle\n );\n\n if (result.error) {\n const error = context.dump(result.error);\n result.error.dispose();\n throw new Error(`Fetch handler error: ${JSON.stringify(error)}`);\n }\n\n let responseHandle = result.value;\n\n // Check if result is a Promise\n const typeofResult = context.typeof(responseHandle);\n if (typeofResult === \"object\") {\n // Check for .then method (Promise-like)\n const thenHandle = context.getProp(responseHandle, \"then\");\n const hasThen = context.typeof(thenHandle) === \"function\";\n thenHandle.dispose();\n\n if (hasThen) {\n // Execute pending jobs BEFORE resolvePromise to allow\n // JavaScript-native async functions to schedule their resolution.\n // This is critical for async functions that don't use host-side awaits.\n context.runtime.executePendingJobs();\n\n // For pure JS async functions (without host-side awaits), we need to\n // wrap resolvePromise in a race with repeated executePendingJobs calls.\n // The sync variant of QuickJS needs this to process microtasks.\n const resolved = await Promise.race([\n context.resolvePromise(responseHandle),\n (async () => {\n // Keep executing pending jobs until the promise resolves\n // This handles pure JS async functions that don't trigger host callbacks\n for (let i = 0; i < 1000; i++) {\n await new Promise((r) => setTimeout(r, 0));\n context.runtime.executePendingJobs();\n }\n // If we get here, something is very wrong\n throw new Error(\"Promise resolution timeout\");\n })(),\n ]);\n responseHandle.dispose();\n context.runtime.executePendingJobs();\n\n if (resolved.error) {\n const error = context.dump(resolved.error);\n resolved.error.dispose();\n throw new Error(`Fetch handler promise rejected: ${JSON.stringify(error)}`);\n }\n responseHandle = resolved.value;\n }\n }\n\n // Get the ResponseState directly from the instance\n const responseState = getInstanceState<ResponseState>(context, responseHandle);\n responseHandle.dispose();\n\n if (!responseState) {\n throw new Error(\"Failed to get Response state\");\n }\n\n // Convert to native Response\n return responseStateToNative(responseState);\n } finally {\n requestHandle.dispose();\n serverHandle.dispose();\n }\n },\n\n getUpgradeRequest(): UpgradeRequest | null {\n return serveState.pendingUpgrade;\n },\n\n /**\n * Dispatch WebSocket open event to the QuickJS handler.\n *\n * **IMPORTANT:** An `open` handler MUST be defined in the serve() websocket\n * options for the connection to be tracked. Without an open handler, the\n * connection is not stored and subsequent message/close/error events will\n * be silently ignored.\n *\n * @param connectionId - Unique identifier for this WebSocket connection (host-assigned)\n * @param data - Optional user data from server.upgrade() to associate with the connection\n */\n dispatchWebSocketOpen(connectionId: string, data?: unknown): void {\n if (!serveState.websocketHandlers.open) {\n return;\n }\n\n // Create ServerWebSocket instance using evalCode (classes must be called with 'new')\n // Pass arguments through temporary globals\n const connectionIdHandle = context.newString(connectionId);\n const dataHandle = marshal(context, data);\n context.setProp(context.global, \"__wsConnectionId__\", connectionIdHandle);\n context.setProp(context.global, \"__wsData__\", dataHandle);\n connectionIdHandle.dispose();\n dataHandle.dispose();\n\n const wsResult = context.evalCode(`new __ServerWebSocket__(__wsConnectionId__, __wsData__)`);\n\n // Clean up temporary globals\n context.setProp(context.global, \"__wsConnectionId__\", context.undefined);\n context.setProp(context.global, \"__wsData__\", context.undefined);\n\n if (wsResult.error) {\n wsResult.error.dispose();\n return;\n }\n\n const wsHandle = wsResult.value;\n\n // Store the connection\n serveState.activeConnections.set(connectionId, {\n data,\n readyState: 1,\n connectionId,\n wsHandle,\n });\n\n // Call the open handler\n const result = context.callFunction(\n serveState.websocketHandlers.open,\n context.undefined,\n wsHandle\n );\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket message event to the QuickJS handler.\n *\n * Requires the connection to have been tracked via dispatchWebSocketOpen().\n * If no message handler is defined, or the connection is not tracked, this is a no-op.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param message - The message content (string or binary data)\n */\n dispatchWebSocketMessage(\n connectionId: string,\n message: string | ArrayBuffer\n ): void {\n if (!serveState.websocketHandlers.message) {\n return;\n }\n\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n return;\n }\n\n const messageHandle =\n typeof message === \"string\"\n ? context.newString(message)\n : context.newArrayBuffer(message);\n\n const result = context.callFunction(\n serveState.websocketHandlers.message,\n context.undefined,\n connection.wsHandle,\n messageHandle\n );\n\n messageHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket close event to the QuickJS handler and clean up the connection.\n *\n * Updates the connection ready state to CLOSED (3), calls the close handler if defined,\n * disposes the WebSocket handle, and removes the connection from tracking.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param code - The WebSocket close code (e.g., 1000 for normal closure)\n * @param reason - The close reason string\n */\n dispatchWebSocketClose(\n connectionId: string,\n code: number,\n reason: string\n ): void {\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n serveState.activeConnections.delete(connectionId);\n return;\n }\n\n if (!serveState.websocketHandlers.close) {\n // No close handler, but still need to cleanup the connection\n connection.wsHandle.dispose();\n serveState.activeConnections.delete(connectionId);\n return;\n }\n\n // Update ready state\n connection.readyState = 3; // CLOSED\n\n const codeHandle = context.newNumber(code);\n const reasonHandle = context.newString(reason);\n\n const result = context.callFunction(\n serveState.websocketHandlers.close,\n context.undefined,\n connection.wsHandle,\n codeHandle,\n reasonHandle\n );\n\n codeHandle.dispose();\n reasonHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n // Cleanup\n connection.wsHandle.dispose();\n serveState.activeConnections.delete(connectionId);\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Dispatch WebSocket error event to the QuickJS handler.\n *\n * Requires the connection to have been tracked via dispatchWebSocketOpen().\n * If no error handler is defined, or the connection is not tracked, this is a no-op.\n *\n * @param connectionId - The connection ID from dispatchWebSocketOpen()\n * @param error - The error that occurred\n */\n dispatchWebSocketError(connectionId: string, error: Error): void {\n if (!serveState.websocketHandlers.error) {\n return;\n }\n\n const connection = serveState.activeConnections.get(connectionId);\n if (!connection || !connection.wsHandle) {\n return;\n }\n\n const errorHandle = marshal(context, {\n name: error.name,\n message: error.message,\n });\n\n const result = context.callFunction(\n serveState.websocketHandlers.error,\n context.undefined,\n connection.wsHandle,\n errorHandle\n );\n\n errorHandle.dispose();\n\n if (result.error) {\n result.error.dispose();\n } else {\n result.value.dispose();\n }\n\n context.runtime.executePendingJobs();\n },\n\n /**\n * Register a callback for outgoing WebSocket commands from QuickJS.\n *\n * Called when QuickJS code calls ws.send() or ws.close() on a ServerWebSocket.\n * The callback receives command objects that should be forwarded to the actual WebSocket.\n *\n * @param callback - Function to handle outgoing WebSocket commands\n * @returns Unsubscribe function to remove the callback\n */\n onWebSocketCommand(\n callback: (command: WebSocketCommand) => void\n ): () => void {\n wsCommandCallbacks.add(callback);\n return () => wsCommandCallbacks.delete(callback);\n },\n\n hasServeHandler(): boolean {\n return serveState.fetchHandler !== null;\n },\n\n dispose(): void {\n // Dispose WebSocket handler handles\n if (serveState.websocketHandlers.open) {\n serveState.websocketHandlers.open.dispose();\n serveState.websocketHandlers.open = undefined;\n }\n if (serveState.websocketHandlers.message) {\n serveState.websocketHandlers.message.dispose();\n serveState.websocketHandlers.message = undefined;\n }\n if (serveState.websocketHandlers.close) {\n serveState.websocketHandlers.close.dispose();\n serveState.websocketHandlers.close = undefined;\n }\n if (serveState.websocketHandlers.error) {\n serveState.websocketHandlers.error.dispose();\n serveState.websocketHandlers.error = undefined;\n }\n\n // Dispose fetch handler handle\n if (serveState.fetchHandler) {\n serveState.fetchHandler.dispose();\n serveState.fetchHandler = null;\n }\n\n // Dispose active WebSocket connection handles\n for (const connection of serveState.activeConnections.values()) {\n if (connection.wsHandle) {\n connection.wsHandle.dispose();\n }\n }\n serveState.activeConnections.clear();\n\n // Note: __Server__ and __ServerWebSocket__ are on global and will be cleaned up by context.dispose()\n },\n };\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;AACA;AAWA;AACA;AAKA,SAAS,qBAAqB,CAC5B,SACA,UACA,cACe;AAAA,EAGf,MAAM,YAAY,QAAQ,UAAU,aAAa,GAAG;AAAA,EACpD,QAAQ,QAAQ,QAAQ,QAAQ,kBAAkB,SAAS;AAAA,EAC3D,UAAU,QAAQ;AAAA,EAElB,MAAM,gBAAgB,QAAQ,SAAS,6BAA6B;AAAA,EAGpE,QAAQ,QAAQ,QAAQ,QAAQ,kBAAkB,QAAQ,SAAS;AAAA,EAEnE,IAAI,cAAc,OAAO;AAAA,IACvB,MAAM,QAAQ,QAAQ,KAAK,cAAc,KAAK;AAAA,IAC9C,cAAc,MAAM,QAAQ;AAAA,IAC5B,MAAM,IAAI,MAAM,6BAA6B,KAAK,UAAU,KAAK,GAAG;AAAA,EACtE;AAAA,EAEA,MAAM,gBAAgB,cAAc;AAAA,EAGpC,iBAAiB,SAAS,eAAe,YAAY;AAAA,EAErD,OAAO;AAAA;AAMF,SAAS,iBAAiB,CAC/B,SACA,UACA,YACa;AAAA,EACb,MAAM,qBAAqB,IAAI;AAAA,EAE/B,OAAO;AAAA,IACL;AAAA,SAEM,gBAAe,CAAC,SAAqC;AAAA,MACzD,IAAI,CAAC,WAAW,cAAc;AAAA,QAC5B,MAAM,IAAI,MAAM,+BAA+B;AAAA,MACjD;AAAA,MAGA,WAAW,iBAAiB;AAAA,MAG5B,MAAM,eAAe,MAAM,6BAA6B,OAAO;AAAA,MAG/D,MAAM,gBAAgB,sBAAsB,SAAS,UAAU,YAAY;AAAA,MAG3E,MAAM,eAAe,QAAQ,SAAS,kBAAkB;AAAA,MACxD,IAAI,aAAa,OAAO;AAAA,QACtB,cAAc,QAAQ;AAAA,QACtB,MAAM,QAAQ,QAAQ,KAAK,aAAa,KAAK;AAAA,QAC7C,aAAa,MAAM,QAAQ;AAAA,QAC3B,MAAM,IAAI,MAAM,4BAA4B,OAAO;AAAA,MACrD;AAAA,MACA,MAAM,eAAe,aAAa;AAAA,MAElC,IAAI;AAAA,QAEF,MAAM,SAAS,QAAQ,aACrB,WAAW,cACX,QAAQ,WACR,eACA,YACF;AAAA,QAEA,IAAI,OAAO,OAAO;AAAA,UAChB,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK;AAAA,UACvC,OAAO,MAAM,QAAQ;AAAA,UACrB,MAAM,IAAI,MAAM,wBAAwB,KAAK,UAAU,KAAK,GAAG;AAAA,QACjE;AAAA,QAEA,IAAI,iBAAiB,OAAO;AAAA,QAG5B,MAAM,eAAe,QAAQ,OAAO,cAAc;AAAA,QAClD,IAAI,iBAAiB,UAAU;AAAA,UAE7B,MAAM,aAAa,QAAQ,QAAQ,gBAAgB,MAAM;AAAA,UACzD,MAAM,UAAU,QAAQ,OAAO,UAAU,MAAM;AAAA,UAC/C,WAAW,QAAQ;AAAA,UAEnB,IAAI,SAAS;AAAA,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "C4B0D934788B3BC064756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/mjs/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/quickjs-fetch",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"main": "./dist/cjs/index.cjs",
|
|
5
5
|
"types": "./dist/types/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"typecheck": "tsc --noEmit"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@ricsam/quickjs-core": "^0.2.
|
|
19
|
+
"@ricsam/quickjs-core": "^0.2.4",
|
|
20
20
|
"quickjs-emscripten": "^0.31.0"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|