diff options
Diffstat (limited to 'src/js/builtins')
31 files changed, 8780 insertions, 0 deletions
diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h new file mode 100644 index 000000000..640d122ca --- /dev/null +++ b/src/js/builtins/BunBuiltinNames.h @@ -0,0 +1,267 @@ +#pragma once + +#include "JavaScriptCore/BuiltinUtils.h" +#include "root.h" + +namespace WebCore { + +using namespace JSC; + +#if !defined(BUN_ADDITIONAL_PRIVATE_IDENTIFIERS) +#define BUN_ADDITIONAL_PRIVATE_IDENTIFIERS(macro) +#endif + +#define BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME(macro) \ + macro(AbortSignal) \ + macro(Buffer) \ + macro(Bun) \ + macro(Loader) \ + macro(ReadableByteStreamController) \ + macro(ReadableStream) \ + macro(ReadableStreamBYOBReader) \ + macro(ReadableStreamBYOBRequest) \ + macro(ReadableStreamDefaultController) \ + macro(ReadableStreamDefaultReader) \ + macro(TransformStream) \ + macro(TransformStreamDefaultController) \ + macro(WritableStream) \ + macro(WritableStreamDefaultController) \ + macro(WritableStreamDefaultWriter) \ + macro(_events) \ + macro(abortAlgorithm) \ + macro(abortSteps) \ + macro(addEventListener) \ + macro(appendFromJS) \ + macro(argv) \ + macro(assignToStream) \ + macro(associatedReadableByteStreamController) \ + macro(autoAllocateChunkSize) \ + macro(backpressure) \ + macro(backpressureChangePromise) \ + macro(basename) \ + macro(body) \ + macro(bunNativePtr) \ + macro(bunNativeType) \ + macro(byobRequest) \ + macro(cancel) \ + macro(cancelAlgorithm) \ + macro(chdir) \ + macro(cloneArrayBuffer) \ + macro(close) \ + macro(closeAlgorithm) \ + macro(closeRequest) \ + macro(closeRequested) \ + macro(closed) \ + macro(closedPromise) \ + macro(closedPromiseCapability) \ + macro(code) \ + macro(commonJSSymbol) \ + macro(connect) \ + macro(consumeReadableStream) \ + macro(controlledReadableStream) \ + macro(controller) \ + macro(cork) \ + macro(createEmptyReadableStream) \ + macro(createFIFO) \ + macro(createNativeReadableStream) \ + macro(createReadableStream) \ + macro(createUninitializedArrayBuffer) \ + macro(createWritableStreamFromInternal) \ + macro(cwd) \ + macro(data) \ + macro(dataView) \ + macro(decode) \ + macro(delimiter) \ + macro(destroy) \ + macro(dir) \ + macro(direct) \ + macro(dirname) \ + macro(disturbed) \ + macro(document) \ + macro(encode) \ + macro(encoding) \ + macro(end) \ + macro(errno) \ + macro(errorSteps) \ + macro(execArgv) \ + macro(exports) \ + macro(extname) \ + macro(failureKind) \ + macro(fatal) \ + macro(fetch) \ + macro(fetchRequest) \ + macro(file) \ + macro(filePath) \ + macro(fillFromJS) \ + macro(filter) \ + macro(finishConsumingStream) \ + macro(flush) \ + macro(flushAlgorithm) \ + macro(format) \ + macro(fulfillModuleSync) \ + macro(get) \ + macro(getInternalWritableStream) \ + macro(handleEvent) \ + macro(hash) \ + macro(header) \ + macro(headers) \ + macro(highWaterMark) \ + macro(host) \ + macro(hostname) \ + macro(href) \ + macro(ignoreBOM) \ + macro(importer) \ + macro(inFlightCloseRequest) \ + macro(inFlightWriteRequest) \ + macro(initializeWith) \ + macro(internalRequire) \ + macro(internalStream) \ + macro(internalWritable) \ + macro(isAbortSignal) \ + macro(isAbsolute) \ + macro(isDisturbed) \ + macro(isPaused) \ + macro(isWindows) \ + macro(join) \ + macro(kind) \ + macro(lazy) \ + macro(lazyLoad) \ + macro(lazyStreamPrototypeMap) \ + macro(loadCJS2ESM) \ + macro(loadModule) \ + macro(localStreams) \ + macro(main) \ + macro(makeDOMException) \ + macro(makeGetterTypeError) \ + macro(makeThisTypeError) \ + macro(map) \ + macro(method) \ + macro(nextTick) \ + macro(normalize) \ + macro(on) \ + macro(once) \ + macro(options) \ + macro(origin) \ + macro(ownerReadableStream) \ + macro(parse) \ + macro(password) \ + macro(patch) \ + macro(path) \ + macro(pathname) \ + macro(pause) \ + macro(pendingAbortRequest) \ + macro(pendingPullIntos) \ + macro(pid) \ + macro(pipe) \ + macro(port) \ + macro(post) \ + macro(ppid) \ + macro(prependEventListener) \ + macro(process) \ + macro(protocol) \ + macro(pull) \ + macro(pullAgain) \ + macro(pullAlgorithm) \ + macro(pulling) \ + macro(put) \ + macro(queue) \ + macro(read) \ + macro(readIntoRequests) \ + macro(readRequests) \ + macro(readable) \ + macro(readableStreamController) \ + macro(readableStreamToArray) \ + macro(reader) \ + macro(readyPromise) \ + macro(readyPromiseCapability) \ + macro(redirect) \ + macro(relative) \ + macro(releaseLock) \ + macro(removeEventListener) \ + macro(require) \ + macro(requireESM) \ + macro(requireMap) \ + macro(resolve) \ + macro(resolveSync) \ + macro(resume) \ + macro(search) \ + macro(searchParams) \ + macro(self) \ + macro(sep) \ + macro(setBody) \ + macro(setStatus) \ + macro(setup) \ + macro(sink) \ + macro(size) \ + macro(start) \ + macro(startAlgorithm) \ + macro(startConsumingStream) \ + macro(startDirectStream) \ + macro(started) \ + macro(startedPromise) \ + macro(state) \ + macro(status) \ + macro(storedError) \ + macro(strategy) \ + macro(strategyHWM) \ + macro(strategySizeAlgorithm) \ + macro(stream) \ + macro(streamClosed) \ + macro(streamClosing) \ + macro(streamErrored) \ + macro(streamReadable) \ + macro(streamWaiting) \ + macro(streamWritable) \ + macro(structuredCloneForStream) \ + macro(syscall) \ + macro(textDecoderStreamDecoder) \ + macro(textDecoderStreamTransform) \ + macro(textEncoderStreamEncoder) \ + macro(textEncoderStreamTransform) \ + macro(toNamespacedPath) \ + macro(trace) \ + macro(transformAlgorithm) \ + macro(uncork) \ + macro(underlyingByteSource) \ + macro(underlyingSink) \ + macro(underlyingSource) \ + macro(unpipe) \ + macro(unshift) \ + macro(url) \ + macro(username) \ + macro(version) \ + macro(versions) \ + macro(view) \ + macro(whenSignalAborted) \ + macro(writable) \ + macro(write) \ + macro(writeAlgorithm) \ + macro(writeRequests) \ + macro(writer) \ + macro(writing) \ + macro(written) \ + BUN_ADDITIONAL_PRIVATE_IDENTIFIERS(macro) \ + +class BunBuiltinNames { +public: + // FIXME: Remove the __attribute__((nodebug)) when <rdar://68246686> is fixed. +#if COMPILER(CLANG) + __attribute__((nodebug)) +#endif + explicit BunBuiltinNames(JSC::VM& vm) + : m_vm(vm) + BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME(INITIALIZE_BUILTIN_NAMES) + { +#define EXPORT_NAME(name) m_vm.propertyNames->appendExternalName(name##PublicName(), name##PrivateName()); + BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME(EXPORT_NAME) +#undef EXPORT_NAME + } + + BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME(DECLARE_BUILTIN_IDENTIFIER_ACCESSOR) + +private: + JSC::VM& m_vm; + BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME(DECLARE_BUILTIN_NAMES) +}; + +} // namespace WebCore diff --git a/src/js/builtins/BundlerPlugin.ts b/src/js/builtins/BundlerPlugin.ts new file mode 100644 index 000000000..831a6614e --- /dev/null +++ b/src/js/builtins/BundlerPlugin.ts @@ -0,0 +1,370 @@ +import type { + AnyFunction, + BuildConfig, + BunPlugin, + OnLoadCallback, + OnLoadResult, + OnLoadResultObject, + OnLoadResultSourceCode, + OnResolveCallback, + PluginBuilder, + PluginConstraints, +} from "bun"; + +// This API expects 4 functions: +// It should be generic enough to reuse for Bun.plugin() eventually, too. +interface BundlerPlugin { + onLoad: Map<string, [RegExp, OnLoadCallback][]>; + onResolve: Map<string, [RegExp, OnResolveCallback][]>; + onLoadAsync( + internalID, + sourceCode: string | Uint8Array | ArrayBuffer | DataView | null, + loaderKey: number | null, + ): void; + onResolveAsync(internalID, a, b, c): void; + addError(internalID, error, number): void; + addFilter(filter, namespace, number): void; +} + +// Extra types +type Setup = BunPlugin["setup"]; +type MinifyObj = Exclude<BuildConfig["minify"], boolean>; +interface BuildConfigExt extends BuildConfig { + // we support esbuild-style entryPoints + entryPoints?: string[]; + // plugins is guaranteed to not be null + plugins: BunPlugin[]; +} +interface PluginBuilderExt extends PluginBuilder { + // these functions aren't implemented yet, so we dont publicly expose them + resolve: AnyFunction; + onStart: AnyFunction; + onEnd: AnyFunction; + onDispose: AnyFunction; + // we partially support initialOptions. it's read-only and a subset of + // all options mapped to their esbuild names + initialOptions: any; + // we set this to an empty object + esbuild: any; +} + +export function runSetupFunction(this: BundlerPlugin, setup: Setup, config: BuildConfigExt) { + var onLoadPlugins = new Map<string, [RegExp, AnyFunction][]>(); + var onResolvePlugins = new Map<string, [RegExp, AnyFunction][]>(); + + function validate(filterObject: PluginConstraints, callback, map) { + if (!filterObject || !$isObject(filterObject)) { + throw new TypeError('Expected an object with "filter" RegExp'); + } + + if (!callback || !$isCallable(callback)) { + throw new TypeError("callback must be a function"); + } + + var { filter, namespace = "file" } = filterObject; + + if (!filter) { + throw new TypeError('Expected an object with "filter" RegExp'); + } + + if (!$isRegExpObject(filter)) { + throw new TypeError("filter must be a RegExp"); + } + + if (namespace && !(typeof namespace === "string")) { + throw new TypeError("namespace must be a string"); + } + + if ((namespace?.length ?? 0) === 0) { + namespace = "file"; + } + + if (!/^([/$a-zA-Z0-9_\\-]+)$/.test(namespace)) { + throw new TypeError("namespace can only contain $a-zA-Z0-9_\\-"); + } + + var callbacks = map.$get(namespace); + + if (!callbacks) { + map.$set(namespace, [[filter, callback]]); + } else { + $arrayPush(callbacks, [filter, callback]); + } + } + + function onLoad(filterObject, callback) { + validate(filterObject, callback, onLoadPlugins); + } + + function onResolve(filterObject, callback) { + validate(filterObject, callback, onResolvePlugins); + } + + const processSetupResult = () => { + var anyOnLoad = false, + anyOnResolve = false; + + for (var [namespace, callbacks] of onLoadPlugins.entries()) { + for (var [filter] of callbacks) { + this.addFilter(filter, namespace, 1); + anyOnLoad = true; + } + } + + for (var [namespace, callbacks] of onResolvePlugins.entries()) { + for (var [filter] of callbacks) { + this.addFilter(filter, namespace, 0); + anyOnResolve = true; + } + } + + if (anyOnResolve) { + var onResolveObject = this.onResolve; + if (!onResolveObject) { + this.onResolve = onResolvePlugins; + } else { + for (var [namespace, callbacks] of onResolvePlugins.entries()) { + var existing = onResolveObject.$get(namespace) as [RegExp, AnyFunction][]; + + if (!existing) { + onResolveObject.$set(namespace, callbacks); + } else { + onResolveObject.$set(namespace, existing.concat(callbacks)); + } + } + } + } + + if (anyOnLoad) { + var onLoadObject = this.onLoad; + if (!onLoadObject) { + this.onLoad = onLoadPlugins; + } else { + for (var [namespace, callbacks] of onLoadPlugins.entries()) { + var existing = onLoadObject.$get(namespace) as [RegExp, AnyFunction][]; + + if (!existing) { + onLoadObject.$set(namespace, callbacks); + } else { + onLoadObject.$set(namespace, existing.concat(callbacks)); + } + } + } + } + + return anyOnLoad || anyOnResolve; + }; + + var setupResult = setup({ + config: config, + onDispose: notImplementedIssueFn(2771, "On-dispose callbacks"), + onEnd: notImplementedIssueFn(2771, "On-end callbacks"), + onLoad, + onResolve, + onStart: notImplementedIssueFn(2771, "On-start callbacks"), + resolve: notImplementedIssueFn(2771, "build.resolve()"), + // esbuild's options argument is different, we provide some interop + initialOptions: { + ...config, + bundle: true, + entryPoints: config.entrypoints ?? config.entryPoints ?? [], + minify: typeof config.minify === "boolean" ? config.minify : false, + minifyIdentifiers: config.minify === true || (config.minify as MinifyObj)?.identifiers, + minifyWhitespace: config.minify === true || (config.minify as MinifyObj)?.whitespace, + minifySyntax: config.minify === true || (config.minify as MinifyObj)?.syntax, + outbase: config.root, + platform: config.target === "bun" ? "node" : config.target, + }, + esbuild: {}, + } satisfies PluginBuilderExt as PluginBuilder); + + if (setupResult && $isPromise(setupResult)) { + if ($getPromiseInternalField(setupResult, $promiseFieldFlags) & $promiseStateFulfilled) { + setupResult = $getPromiseInternalField(setupResult, $promiseFieldReactionsOrResult); + } else { + return setupResult.$then(processSetupResult); + } + } + + return processSetupResult(); +} + +export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespace, importer, internalID, kindId) { + // Must be kept in sync with ImportRecord.label + const kind = $ImportKindIdToLabel[kindId]; + + var promiseResult: any = (async (inputPath, inputNamespace, importer, kind) => { + var { onResolve, onLoad } = this; + var results = onResolve.$get(inputNamespace); + if (!results) { + this.onResolveAsync(internalID, null, null, null); + return null; + } + + for (let [filter, callback] of results) { + if (filter.test(inputPath)) { + var result = callback({ + path: inputPath, + importer, + namespace: inputNamespace, + // resolveDir + kind, + // pluginData + }); + + while ( + result && + $isPromise(result) && + ($getPromiseInternalField(result, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled + ) { + result = $getPromiseInternalField(result, $promiseFieldReactionsOrResult); + } + + if (result && $isPromise(result)) { + result = await result; + } + + if (!result || !$isObject(result)) { + continue; + } + + var { path, namespace: userNamespace = inputNamespace, external } = result; + if (!(typeof path === "string") || !(typeof userNamespace === "string")) { + throw new TypeError("onResolve plugins must return an object with a string 'path' and string 'loader' field"); + } + + if (!path) { + continue; + } + + if (!userNamespace) { + userNamespace = inputNamespace; + } + if (typeof external !== "boolean" && !$isUndefinedOrNull(external)) { + throw new TypeError('onResolve plugins "external" field must be boolean or unspecified'); + } + + if (!external) { + if (userNamespace === "file") { + if (process.platform !== "win32") { + if (path[0] !== "/" || path.includes("..")) { + throw new TypeError('onResolve plugin "path" must be absolute when the namespace is "file"'); + } + } else { + // TODO: Windows + } + } + if (userNamespace === "dataurl") { + if (!path.startsWith("data:")) { + throw new TypeError('onResolve plugin "path" must start with "data:" when the namespace is "dataurl"'); + } + } + + if (userNamespace && userNamespace !== "file" && (!onLoad || !onLoad.$has(userNamespace))) { + throw new TypeError(`Expected onLoad plugin for namespace ${userNamespace} to exist`); + } + } + this.onResolveAsync(internalID, path, userNamespace, external); + return null; + } + } + + this.onResolveAsync(internalID, null, null, null); + return null; + })(specifier, inputNamespace, importer, kind); + + while ( + promiseResult && + $isPromise(promiseResult) && + ($getPromiseInternalField(promiseResult, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled + ) { + promiseResult = $getPromiseInternalField(promiseResult, $promiseFieldReactionsOrResult); + } + + if (promiseResult && $isPromise(promiseResult)) { + promiseResult.then( + () => {}, + e => { + this.addError(internalID, e, 0); + }, + ); + } +} + +export function runOnLoadPlugins(this: BundlerPlugin, internalID, path, namespace, defaultLoaderId) { + const LOADERS_MAP = $LoaderLabelToId; + const loaderName = $LoaderIdToLabel[defaultLoaderId]; + + var promiseResult = (async (internalID, path, namespace, defaultLoader) => { + var results = this.onLoad.$get(namespace); + if (!results) { + this.onLoadAsync(internalID, null, null); + return null; + } + + for (let [filter, callback] of results) { + if (filter.test(path)) { + var result = callback({ + path, + namespace, + // suffix + // pluginData + loader: defaultLoader, + }); + + while ( + result && + $isPromise(result) && + ($getPromiseInternalField(result, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled + ) { + result = $getPromiseInternalField(result, $promiseFieldReactionsOrResult); + } + + if (result && $isPromise(result)) { + result = await result; + } + + if (!result || !$isObject(result)) { + continue; + } + + var { contents, loader = defaultLoader } = result as OnLoadResultSourceCode & OnLoadResultObject; + if (!(typeof contents === "string") && !$isTypedArrayView(contents)) { + throw new TypeError('onLoad plugins must return an object with "contents" as a string or Uint8Array'); + } + + if (!(typeof loader === "string")) { + throw new TypeError('onLoad plugins must return an object with "loader" as a string'); + } + + const chosenLoader = LOADERS_MAP[loader]; + if (chosenLoader === undefined) { + throw new TypeError(`Loader ${loader} is not supported.`); + } + + this.onLoadAsync(internalID, contents, chosenLoader); + return null; + } + } + + this.onLoadAsync(internalID, null, null); + return null; + })(internalID, path, namespace, loaderName); + + while ( + promiseResult && + $isPromise(promiseResult) && + ($getPromiseInternalField(promiseResult, $promiseFieldFlags) & $promiseStateMask) === $promiseStateFulfilled + ) { + promiseResult = $getPromiseInternalField(promiseResult, $promiseFieldReactionsOrResult); + } + + if (promiseResult && $isPromise(promiseResult)) { + promiseResult.then( + () => {}, + e => { + this.addError(internalID, e, 1); + }, + ); + } +} diff --git a/src/js/builtins/ByteLengthQueuingStrategy.ts b/src/js/builtins/ByteLengthQueuingStrategy.ts new file mode 100644 index 000000000..fc3f3d998 --- /dev/null +++ b/src/js/builtins/ByteLengthQueuingStrategy.ts @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 Canon Inc. + * Copyright (C) 2015 Igalia S.L. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +$getter; +export function highWaterMark(this: any) { + const highWaterMark = $getByIdDirectPrivate(this, "highWaterMark"); + if (highWaterMark === undefined) + throw new TypeError("ByteLengthQueuingStrategy.highWaterMark getter called on incompatible |this| value."); + + return highWaterMark; +} + +export function size(chunk) { + return chunk.byteLength; +} + +export function initializeByteLengthQueuingStrategy(this: any, parameters: any) { + $putByIdDirectPrivate(this, "highWaterMark", $extractHighWaterMarkFromQueuingStrategyInit(parameters)); +} diff --git a/src/js/builtins/ConsoleObject.ts b/src/js/builtins/ConsoleObject.ts new file mode 100644 index 000000000..45746459a --- /dev/null +++ b/src/js/builtins/ConsoleObject.ts @@ -0,0 +1,84 @@ +$overriddenName = "[Symbol.asyncIterator]"; +export function asyncIterator(this: Console) { + const Iterator = async function* ConsoleAsyncIterator() { + const stream = Bun.stdin.stream(); + var reader = stream.getReader(); + + // TODO: use builtin + var decoder = new (globalThis as any).TextDecoder("utf-8", { fatal: false }) as TextDecoder; + var deferredError; + var indexOf = Bun.indexOfLine; + + try { + while (true) { + var done, value; + var pendingChunk; + const firstResult = reader.readMany(); + if ($isPromise(firstResult)) { + ({ done, value } = await firstResult); + } else { + ({ done, value } = firstResult); + } + + if (done) { + if (pendingChunk) { + yield decoder.decode(pendingChunk); + } + return; + } + + var actualChunk; + // we assume it was given line-by-line + for (const chunk of value) { + actualChunk = chunk; + if (pendingChunk) { + actualChunk = Buffer.concat([pendingChunk, chunk]); + pendingChunk = null; + } + + var last = 0; + // TODO: "\r", 0x4048, 0x4049, 0x404A, 0x404B, 0x404C, 0x404D, 0x404E, 0x404F + var i = indexOf(actualChunk, last); + while (i !== -1) { + yield decoder.decode(actualChunk.subarray(last, i)); + last = i + 1; + i = indexOf(actualChunk, last); + } + + pendingChunk = actualChunk.subarray(last); + } + } + } catch (e) { + deferredError = e; + } finally { + reader.releaseLock(); + + if (deferredError) { + throw deferredError; + } + } + }; + + const symbol = globalThis.Symbol.asyncIterator; + this[symbol] = Iterator; + return Iterator(); +} + +export function write(this: Console, input) { + var writer = $getByIdDirectPrivate(this, "writer"); + if (!writer) { + var length = $toLength(input?.length ?? 0); + writer = Bun.stdout.writer({ highWaterMark: length > 65536 ? length : 65536 }); + $putByIdDirectPrivate(this, "writer", writer); + } + + var wrote = writer.write(input); + + const count = $argumentCount(); + for (var i = 1; i < count; i++) { + wrote += writer.write($argument(i)); + } + + writer.flush(true); + return wrote; +} diff --git a/src/js/builtins/CountQueuingStrategy.ts b/src/js/builtins/CountQueuingStrategy.ts new file mode 100644 index 000000000..a72dca1ca --- /dev/null +++ b/src/js/builtins/CountQueuingStrategy.ts @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 Canon Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +$getter; +export function highWaterMark(this: any) { + const highWaterMark = $getByIdDirectPrivate(this, "highWaterMark"); + + if (highWaterMark === undefined) + throw new TypeError("CountQueuingStrategy.highWaterMark getter called on incompatible |this| value."); + + return highWaterMark; +} + +export function size() { + return 1; +} + +export function initializeCountQueuingStrategy(this: any, parameters: any) { + $putByIdDirectPrivate(this, "highWaterMark", $extractHighWaterMarkFromQueuingStrategyInit(parameters)); +} diff --git a/src/js/builtins/ImportMetaObject.ts b/src/js/builtins/ImportMetaObject.ts new file mode 100644 index 000000000..2df0f0c98 --- /dev/null +++ b/src/js/builtins/ImportMetaObject.ts @@ -0,0 +1,257 @@ +type ImportMetaObject = Partial<ImportMeta>; + +export function loadCJS2ESM(this: ImportMetaObject, resolvedSpecifier: string) { + var loader = Loader; + var queue = $createFIFO(); + var key = resolvedSpecifier; + while (key) { + // we need to explicitly check because state could be $ModuleFetch + // it will throw this error if we do not: + // $throwTypeError("Requested module is already fetched."); + var entry = loader.registry.$get(key); + + if (!entry || !entry.state || entry.state <= $ModuleFetch) { + $fulfillModuleSync(key); + entry = loader.registry.$get(key)!; + } + + // entry.fetch is a Promise<SourceCode> + // SourceCode is not a string, it's a JSC::SourceCode object + // this pulls it out of the promise without delaying by a tick + // the promise is already fullfilled by $fullfillModuleSync + var sourceCodeObject = $getPromiseInternalField(entry.fetch, $promiseFieldReactionsOrResult); + // parseModule() returns a Promise, but the value is already fulfilled + // so we just pull it out of the promise here once again + // But, this time we do it a little more carefully because this is a JSC function call and not bun source code + var moduleRecordPromise = loader.parseModule(key, sourceCodeObject); + var module = entry.module; + if (!module && moduleRecordPromise && $isPromise(moduleRecordPromise)) { + var reactionsOrResult = $getPromiseInternalField(moduleRecordPromise, $promiseFieldReactionsOrResult); + var flags = $getPromiseInternalField(moduleRecordPromise, $promiseFieldFlags); + var state = flags & $promiseStateMask; + // this branch should never happen, but just to be safe + if (state === $promiseStatePending || (reactionsOrResult && $isPromise(reactionsOrResult))) { + throw new TypeError(`require() async module "${key}" is unsupported`); + } else if (state === $promiseStateRejected) { + // TODO: use SyntaxError but preserve the specifier + throw new TypeError(`${reactionsOrResult?.message ?? "An error occurred"} while parsing module \"${key}\"`); + } + entry.module = module = reactionsOrResult; + } else if (moduleRecordPromise && !module) { + entry.module = module = moduleRecordPromise as LoaderModule; + } + + // This is very similar to "requestInstantiate" in ModuleLoader.js in JavaScriptCore. + $setStateToMax(entry, $ModuleLink); + var dependenciesMap = module.dependenciesMap; + var requestedModules = loader.requestedModules(module); + var dependencies = $newArrayWithSize<string>(requestedModules.length); + for (var i = 0, length = requestedModules.length; i < length; ++i) { + var depName = requestedModules[i]; + // optimization: if it starts with a slash then it's an absolute path + // we don't need to run the resolver a 2nd time + var depKey = depName[0] === "/" ? depName : loader.resolve(depName, key); + var depEntry = loader.ensureRegistered(depKey); + if (depEntry.state < $ModuleLink) { + queue.push(depKey); + } + + $putByValDirect(dependencies, i, depEntry); + dependenciesMap.$set(depName, depEntry); + } + + entry.dependencies = dependencies; + // All dependencies resolved, set instantiate and satisfy field directly. + entry.instantiate = Promise.resolve(entry); + entry.satisfy = Promise.resolve(entry); + key = queue.shift(); + while (key && (loader.registry.$get(key)?.state ?? $ModuleFetch) >= $ModuleLink) { + key = queue.shift(); + } + } + + var linkAndEvaluateResult = loader.linkAndEvaluateModule(resolvedSpecifier, undefined); + if (linkAndEvaluateResult && $isPromise(linkAndEvaluateResult)) { + // if you use top-level await, or any dependencies use top-level await, then we throw here + // this means the module will still actually load eventually, but that's okay. + throw new TypeError(`require() async module \"${resolvedSpecifier}\" is unsupported`); + } + + return loader.registry.$get(resolvedSpecifier); +} + +export function requireESM(this: ImportMetaObject, resolved) { + var entry = Loader.registry.$get(resolved); + + if (!entry || !entry.evaluated) { + entry = $loadCJS2ESM(resolved); + } + + if (!entry || !entry.evaluated || !entry.module) { + throw new TypeError(`require() failed to evaluate module "${resolved}". This is an internal consistentency error.`); + } + var exports = Loader.getModuleNamespaceObject(entry.module); + if (exports[$commonJSSymbol] === 0) { + // CommonJS module created via `Bun::CommonJSModuleRecord` + // We will refer to the requireMap to get the exports + return; + } + + var commonJS = exports.default; + var cjs = commonJS?.[$commonJSSymbol]; + if (cjs === 0) { + return commonJS; + } else if (cjs && $isCallable(commonJS)) { + return commonJS(); + } + + return exports; +} + +export function internalRequire(this: ImportMetaObject, resolved) { + var cached = $requireMap.$get(resolved); + const last5 = resolved.substring(resolved.length - 5); + if (cached) { + if (last5 === ".node") { + return cached.exports; + } + return cached; + } + + // TODO: remove this hardcoding + if (last5 === ".json") { + var fs = (globalThis[Symbol.for("_fs")] ||= Bun.fs()); + var exports = JSON.parse(fs.readFileSync(resolved, "utf8")); + $requireMap.$set(resolved, exports); + return exports; + } else if (last5 === ".node") { + var module = { exports: {} }; + process.dlopen(module, resolved); + $requireMap.$set(resolved, module); + return module.exports; + } else if (last5 === ".toml") { + var fs = (globalThis[Symbol.for("_fs")] ||= Bun.fs()); + var exports = Bun.TOML.parse(fs.readFileSync(resolved, "utf8")); + $requireMap.$set(resolved, exports); + return exports; + } else { + var exports = $requireESM(resolved); + const cachedExports = $requireMap.$get(resolved); + if (cachedExports) { + return cachedExports; + } + + $requireMap.$set(resolved, exports); + return exports; + } +} + +export function createRequireCache() { + class Module { + id; + parent; + filename; + children = []; + paths = []; + + constructor(filename) { + this.id = filename; + // TODO: windows + const lastSlash = filename.lastIndexOf("/"); + if (lastSlash !== -1 && filename.length > lastSlash + 1) { + this.filename = filename.substring(lastSlash + 1); + } else { + this.filename = filename; + } + } + + get loaded() { + return true; + } + + require(path) { + return $internalRequire($resolveSync(path, this.id)); + } + + get exports() { + return $requireMap.$get(this.id) ?? {}; + } + + set exports(value) { + $requireMap.$set(this.id, value); + } + } + + var moduleMap = new Map(); + + return new Proxy( + {}, + { + get(target, key: string) { + const entry = $requireMap.$get(key); + if (entry) { + var mod = moduleMap.$get(key); + if (!mod) { + mod = new Module(key); + moduleMap.$set(key, mod); + } + return mod; + } + }, + set(target, key: string, value) { + if (!moduleMap.$has(key)) { + moduleMap.$set(key, new Module(key)); + } + + $requireMap.$set(key, value?.exports); + + return true; + }, + + has(target, key: string) { + return $requireMap.$has(key); + }, + + deleteProperty(target, key: string) { + moduleMap.$delete(key); + $requireMap.$delete(key); + Loader.registry.$delete(key); + return true; + }, + + ownKeys(target) { + return [...$requireMap.$keys()]; + }, + + // In Node, require.cache has a null prototype + getPrototypeOf(target) { + return null; + }, + + getOwnPropertyDescriptor(target, key: string) { + if ($requireMap.$has(key)) { + return { + configurable: true, + enumerable: true, + }; + } + }, + }, + ); +} + +$sloppy; +export function require(this: ImportMetaObject, name) { + var from = this?.path ?? arguments.callee.path; + + if (typeof name !== "string") { + throw new TypeError("require(name) must be a string"); + } + + return $internalRequire($resolveSync(name, from)); +} + +$getter; +export function main(this: ImportMetaObject) { + return this.path === Bun.main; +} diff --git a/src/js/builtins/JSBufferConstructor.ts b/src/js/builtins/JSBufferConstructor.ts new file mode 100644 index 000000000..debc62d51 --- /dev/null +++ b/src/js/builtins/JSBufferConstructor.ts @@ -0,0 +1,67 @@ +export function from(items) { + if ($isUndefinedOrNull(items)) { + throw new TypeError( + "The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object.", + ); + } + + // TODO: figure out why private symbol not found + if ( + typeof items === "string" || + (typeof items === "object" && + ($isTypedArrayView(items) || + items instanceof ArrayBuffer || + items instanceof SharedArrayBuffer || + items instanceof String)) + ) { + switch ($argumentCount()) { + case 1: { + return new $Buffer(items); + } + case 2: { + return new $Buffer(items, $argument(1)); + } + default: { + return new $Buffer(items, $argument(1), $argument(2)); + } + } + } + + var arrayLike = $toObject( + items, + "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object.", + ) as ArrayLike<any>; + + if (!$isJSArray(arrayLike)) { + const toPrimitive = $tryGetByIdWithWellKnownSymbol(items, "toPrimitive"); + + if (toPrimitive) { + const primitive = toPrimitive.$call(items, "string"); + + if (typeof primitive === "string") { + switch ($argumentCount()) { + case 1: { + return new $Buffer(primitive); + } + case 2: { + return new $Buffer(primitive, $argument(1)); + } + default: { + return new $Buffer(primitive, $argument(1), $argument(2)); + } + } + } + } + + if (!("length" in arrayLike) || $isCallable(arrayLike)) { + throw new TypeError( + "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object.", + ); + } + } + + // Don't pass the second argument because Node's Buffer.from doesn't accept + // a function and Uint8Array.from requires it if it exists + // That means we cannot use $tailCallFowrardArguments here, sadly + return new $Buffer(Uint8Array.from(arrayLike).buffer); +} diff --git a/src/js/builtins/JSBufferPrototype.ts b/src/js/builtins/JSBufferPrototype.ts new file mode 100644 index 000000000..97b25b9b2 --- /dev/null +++ b/src/js/builtins/JSBufferPrototype.ts @@ -0,0 +1,495 @@ +// The fastest way as of April 2022 is to use DataView. +// DataView has intrinsics that cause inlining + +interface BufferExt extends Buffer { + $dataView?: DataView; + + toString(encoding?: BufferEncoding, start?: number, end?: number): string; + toString(offset: number, length: number, encoding?: BufferEncoding): string; +} + +export function setBigUint64(this: BufferExt, offset, value, le) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigUint64( + offset, + value, + le, + ); +} +export function readInt8(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt8(offset); +} +export function readUInt8(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint8(offset); +} +export function readInt16LE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt16(offset, true); +} +export function readInt16BE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt16(offset, false); +} +export function readUInt16LE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint16(offset, true); +} +export function readUInt16BE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint16(offset, false); +} +export function readInt32LE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt32(offset, true); +} +export function readInt32BE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt32(offset, false); +} +export function readUInt32LE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint32(offset, true); +} +export function readUInt32BE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint32(offset, false); +} + +export function readIntLE(this: BufferExt, offset, byteLength) { + const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { + case 1: { + return view.getInt8(offset); + } + case 2: { + return view.getInt16(offset, true); + } + case 3: { + const val = view.getUint16(offset, true) + view.getUint8(offset + 2) * 2 ** 16; + return val | ((val & (2 ** 23)) * 0x1fe); + } + case 4: { + return view.getInt32(offset, true); + } + case 5: { + const last = view.getUint8(offset + 4); + return (last | ((last & (2 ** 7)) * 0x1fffffe)) * 2 ** 32 + view.getUint32(offset, true); + } + case 6: { + const last = view.getUint16(offset + 4, true); + return (last | ((last & (2 ** 15)) * 0x1fffe)) * 2 ** 32 + view.getUint32(offset, true); + } + } + throw new RangeError("byteLength must be >= 1 and <= 6"); +} +export function readIntBE(this: BufferExt, offset, byteLength) { + const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { + case 1: { + return view.getInt8(offset); + } + case 2: { + return view.getInt16(offset, false); + } + case 3: { + const val = view.getUint16(offset + 1, false) + view.getUint8(offset) * 2 ** 16; + return val | ((val & (2 ** 23)) * 0x1fe); + } + case 4: { + return view.getInt32(offset, false); + } + case 5: { + const last = view.getUint8(offset); + return (last | ((last & (2 ** 7)) * 0x1fffffe)) * 2 ** 32 + view.getUint32(offset + 1, false); + } + case 6: { + const last = view.getUint16(offset, false); + return (last | ((last & (2 ** 15)) * 0x1fffe)) * 2 ** 32 + view.getUint32(offset + 2, false); + } + } + throw new RangeError("byteLength must be >= 1 and <= 6"); +} +export function readUIntLE(this: BufferExt, offset, byteLength) { + const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { + case 1: { + return view.getUint8(offset); + } + case 2: { + return view.getUint16(offset, true); + } + case 3: { + return view.getUint16(offset, true) + view.getUint8(offset + 2) * 2 ** 16; + } + case 4: { + return view.getUint32(offset, true); + } + case 5: { + return view.getUint8(offset + 4) * 2 ** 32 + view.getUint32(offset, true); + } + case 6: { + return view.getUint16(offset + 4, true) * 2 ** 32 + view.getUint32(offset, true); + } + } + throw new RangeError("byteLength must be >= 1 and <= 6"); +} +export function readUIntBE(this: BufferExt, offset, byteLength) { + const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { + case 1: { + return view.getUint8(offset); + } + case 2: { + return view.getUint16(offset, false); + } + case 3: { + return view.getUint16(offset + 1, false) + view.getUint8(offset) * 2 ** 16; + } + case 4: { + return view.getUint32(offset, false); + } + case 5: { + const last = view.getUint8(offset); + return (last | ((last & (2 ** 7)) * 0x1fffffe)) * 2 ** 32 + view.getUint32(offset + 1, false); + } + case 6: { + const last = view.getUint16(offset, false); + return (last | ((last & (2 ** 15)) * 0x1fffe)) * 2 ** 32 + view.getUint32(offset + 2, false); + } + } + throw new RangeError("byteLength must be >= 1 and <= 6"); +} + +export function readFloatLE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat32(offset, true); +} +export function readFloatBE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat32(offset, false); +} +export function readDoubleLE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat64(offset, true); +} +export function readDoubleBE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat64(offset, false); +} +export function readBigInt64LE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigInt64(offset, true); +} +export function readBigInt64BE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigInt64(offset, false); +} +export function readBigUInt64LE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigUint64(offset, true); +} +export function readBigUInt64BE(this: BufferExt, offset) { + return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigUint64(offset, false); +} + +export function writeInt8(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt8(offset, value); + return offset + 1; +} +export function writeUInt8(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint8(offset, value); + return offset + 1; +} +export function writeInt16LE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16(offset, value, true); + return offset + 2; +} +export function writeInt16BE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16(offset, value, false); + return offset + 2; +} +export function writeUInt16LE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16(offset, value, true); + return offset + 2; +} +export function writeUInt16BE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16(offset, value, false); + return offset + 2; +} +export function writeInt32LE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32(offset, value, true); + return offset + 4; +} +export function writeInt32BE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32(offset, value, false); + return offset + 4; +} +export function writeUInt32LE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32(offset, value, true); + return offset + 4; +} +export function writeUInt32BE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32(offset, value, false); + return offset + 4; +} + +export function writeIntLE(this: BufferExt, value, offset, byteLength) { + const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { + case 1: { + view.setInt8(offset, value); + break; + } + case 2: { + view.setInt16(offset, value, true); + break; + } + case 3: { + view.setUint16(offset, value & 0xffff, true); + view.setInt8(offset + 2, Math.floor(value * 2 ** -16)); + break; + } + case 4: { + view.setInt32(offset, value, true); + break; + } + case 5: { + view.setUint32(offset, value | 0, true); + view.setInt8(offset + 4, Math.floor(value * 2 ** -32)); + break; + } + case 6: { + view.setUint32(offset, value | 0, true); + view.setInt16(offset + 4, Math.floor(value * 2 ** -32), true); + break; + } + default: { + throw new RangeError("byteLength must be >= 1 and <= 6"); + } + } + return offset + byteLength; +} +export function writeIntBE(this: BufferExt, value, offset, byteLength) { + const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { + case 1: { + view.setInt8(offset, value); + break; + } + case 2: { + view.setInt16(offset, value, false); + break; + } + case 3: { + view.setUint16(offset + 1, value & 0xffff, false); + view.setInt8(offset, Math.floor(value * 2 ** -16)); + break; + } + case 4: { + view.setInt32(offset, value, false); + break; + } + case 5: { + view.setUint32(offset + 1, value | 0, false); + view.setInt8(offset, Math.floor(value * 2 ** -32)); + break; + } + case 6: { + view.setUint32(offset + 2, value | 0, false); + view.setInt16(offset, Math.floor(value * 2 ** -32), false); + break; + } + default: { + throw new RangeError("byteLength must be >= 1 and <= 6"); + } + } + return offset + byteLength; +} +export function writeUIntLE(this: BufferExt, value, offset, byteLength) { + const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { + case 1: { + view.setUint8(offset, value); + break; + } + case 2: { + view.setUint16(offset, value, true); + break; + } + case 3: { + view.setUint16(offset, value & 0xffff, true); + view.setUint8(offset + 2, Math.floor(value * 2 ** -16)); + break; + } + case 4: { + view.setUint32(offset, value, true); + break; + } + case 5: { + view.setUint32(offset, value | 0, true); + view.setUint8(offset + 4, Math.floor(value * 2 ** -32)); + break; + } + case 6: { + view.setUint32(offset, value | 0, true); + view.setUint16(offset + 4, Math.floor(value * 2 ** -32), true); + break; + } + default: { + throw new RangeError("byteLength must be >= 1 and <= 6"); + } + } + return offset + byteLength; +} +export function writeUIntBE(this: BufferExt, value, offset, byteLength) { + const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); + switch (byteLength) { + case 1: { + view.setUint8(offset, value); + break; + } + case 2: { + view.setUint16(offset, value, false); + break; + } + case 3: { + view.setUint16(offset + 1, value & 0xffff, false); + view.setUint8(offset, Math.floor(value * 2 ** -16)); + break; + } + case 4: { + view.setUint32(offset, value, false); + break; + } + case 5: { + view.setUint32(offset + 1, value | 0, false); + view.setUint8(offset, Math.floor(value * 2 ** -32)); + break; + } + case 6: { + view.setUint32(offset + 2, value | 0, false); + view.setUint16(offset, Math.floor(value * 2 ** -32), false); + break; + } + default: { + throw new RangeError("byteLength must be >= 1 and <= 6"); + } + } + return offset + byteLength; +} + +export function writeFloatLE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32(offset, value, true); + return offset + 4; +} + +export function writeFloatBE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32(offset, value, false); + return offset + 4; +} + +export function writeDoubleLE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64(offset, value, true); + return offset + 8; +} + +export function writeDoubleBE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64(offset, value, false); + return offset + 8; +} + +export function writeBigInt64LE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigInt64(offset, value, true); + return offset + 8; +} + +export function writeBigInt64BE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigInt64(offset, value, false); + return offset + 8; +} + +export function writeBigUInt64LE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigUint64(offset, value, true); + return offset + 8; +} + +export function writeBigUInt64BE(this: BufferExt, value, offset) { + (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigUint64(offset, value, false); + return offset + 8; +} + +export function utf8Write(this: BufferExt, text, offset, length) { + return this.write(text, offset, length, "utf8"); +} +export function ucs2Write(this: BufferExt, text, offset, length) { + return this.write(text, offset, length, "ucs2"); +} +export function utf16leWrite(this: BufferExt, text, offset, length) { + return this.write(text, offset, length, "utf16le"); +} +export function latin1Write(this: BufferExt, text, offset, length) { + return this.write(text, offset, length, "latin1"); +} +export function asciiWrite(this: BufferExt, text, offset, length) { + return this.write(text, offset, length, "ascii"); +} +export function base64Write(this: BufferExt, text, offset, length) { + return this.write(text, offset, length, "base64"); +} +export function base64urlWrite(this: BufferExt, text, offset, length) { + return this.write(text, offset, length, "base64url"); +} +export function hexWrite(this: BufferExt, text, offset, length) { + return this.write(text, offset, length, "hex"); +} + +export function utf8Slice(this: BufferExt, offset, length) { + return this.toString(offset, length, "utf8"); +} +export function ucs2Slice(this: BufferExt, offset, length) { + return this.toString(offset, length, "ucs2"); +} +export function utf16leSlice(this: BufferExt, offset, length) { + return this.toString(offset, length, "utf16le"); +} +export function latin1Slice(this: BufferExt, offset, length) { + return this.toString(offset, length, "latin1"); +} +export function asciiSlice(this: BufferExt, offset, length) { + return this.toString(offset, length, "ascii"); +} +export function base64Slice(this: BufferExt, offset, length) { + return this.toString(offset, length, "base64"); +} +export function base64urlSlice(this: BufferExt, offset, length) { + return this.toString(offset, length, "base64url"); +} +export function hexSlice(this: BufferExt, offset, length) { + return this.toString(offset, length, "hex"); +} + +export function toJSON(this: BufferExt) { + const type = "Buffer"; + const data = Array.from(this); + return { type, data }; +} + +export function slice(this: BufferExt, start, end) { + var { buffer, byteOffset, byteLength } = this; + + function adjustOffset(offset, length) { + // Use Math.trunc() to convert offset to an integer value that can be larger + // than an Int32. Hence, don't use offset | 0 or similar techniques. + offset = $trunc(offset); + if (offset === 0 || isNaN(offset)) { + return 0; + } else if (offset < 0) { + offset += length; + return offset > 0 ? offset : 0; + } else { + return offset < length ? offset : length; + } + } + + var start_ = adjustOffset(start, byteLength); + var end_ = end !== undefined ? adjustOffset(end, byteLength) : byteLength; + return new $Buffer(buffer, byteOffset + start_, end_ > start_ ? end_ - start_ : 0); +} + +$getter; +export function parent(this: BufferExt) { + return $isObject(this) && this instanceof $Buffer ? this.buffer : undefined; +} + +$getter; +export function offset(this: BufferExt) { + return $isObject(this) && this instanceof $Buffer ? this.byteOffset : undefined; +} + +export function inspect(this: BufferExt, recurseTimes, ctx) { + return Bun.inspect(this); +} diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts new file mode 100644 index 000000000..8b24e68ba --- /dev/null +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -0,0 +1,676 @@ +/* + * Copyright 2023 Codeblog Corp. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function binding(bindingName) { + if (bindingName !== "constants") + throw new TypeError( + "process.binding() is not supported in Bun. If that breaks something, please file an issue and include a reproducible code sample.", + ); + + var cache = globalThis.Symbol.for("process.bindings.constants"); + var constants = globalThis[cache]; + if (!constants) { + // TODO: make this less hacky. + // This calls require("node:fs").constants + // except, outside an ESM module. + const { constants: fs } = globalThis[globalThis.Symbol.for("Bun.lazy")]("createImportMeta", "node:process").require( + "node:fs", + ); + constants = { + fs, + zlib: {}, + crypto: {}, + os: Bun._Os().constants, + }; + globalThis[cache] = constants; + } + return constants; +} + +export function getStdioWriteStream(fd_, rawRequire) { + var module = { path: "node:process", require: rawRequire }; + var require = path => module.require(path); + + function createStdioWriteStream(fd_) { + var { Duplex, eos, destroy } = require("node:stream"); + var StdioWriteStream = class StdioWriteStream extends Duplex { + #writeStream; + #readStream; + + #readable = true; + #writable = true; + #fdPath; + + #onClose; + #onDrain; + #onFinish; + #onReadable; + #isTTY; + + get isTTY() { + return (this.#isTTY ??= require("node:tty").isatty(fd_)); + } + + get fd() { + return fd_; + } + + constructor(fd) { + super({ readable: true, writable: true }); + this.#fdPath = `/dev/fd/${fd}`; + } + + #onFinished(err) { + const cb = this.#onClose; + this.#onClose = null; + + if (cb) { + cb(err); + } else if (err) { + this.destroy(err); + } else if (!this.#readable && !this.#writable) { + this.destroy(); + } + } + + _destroy(err, callback) { + if (!err && this.#onClose !== null) { + var AbortError = class AbortError extends Error { + code: string; + name: string; + constructor(message = "The operation was aborted", options = void 0) { + if (options !== void 0 && typeof options !== "object") { + throw new Error(`Invalid AbortError options:\n\n${JSON.stringify(options, null, 2)}`); + } + super(message, options); + this.code = "ABORT_ERR"; + this.name = "AbortError"; + } + }; + err = new AbortError(); + } + + this.#onDrain = null; + this.#onFinish = null; + if (this.#onClose === null) { + callback(err); + } else { + this.#onClose = callback; + if (this.#writeStream) destroy(this.#writeStream, err); + if (this.#readStream) destroy(this.#readStream, err); + } + } + + _write(chunk, encoding, callback) { + if (!this.#writeStream) { + var { createWriteStream } = require("node:fs"); + var stream = (this.#writeStream = createWriteStream(this.#fdPath)); + + stream.on("finish", () => { + if (this.#onFinish) { + const cb = this.#onFinish; + this.#onFinish = null; + cb(); + } + }); + + stream.on("drain", () => { + if (this.#onDrain) { + const cb = this.#onDrain; + this.#onDrain = null; + cb(); + } + }); + + eos(stream, err => { + this.#writable = false; + if (err) { + destroy(stream, err); + } + this.#onFinished(err); + }); + } + if (stream.write(chunk, encoding)) { + callback(); + } else { + this.#onDrain = callback; + } + } + + _final(callback) { + this.#writeStream && this.#writeStream.end(); + this.#onFinish = callback; + } + + #loadReadStream() { + var { createReadStream } = require("node:fs"); + + var readStream = (this.#readStream = createReadStream(this.#fdPath)); + + readStream.on("readable", () => { + if (this.#onReadable) { + const cb = this.#onReadable; + this.#onReadable = null; + cb(); + } else { + this.read(); + } + }); + + readStream.on("end", () => { + this.push(null); + }); + + eos(readStream, err => { + this.#readable = false; + if (err) { + destroy(readStream, err); + } + this.#onFinished(err); + }); + return readStream; + } + + _read() { + var stream = this.#readStream; + if (!stream) { + stream = this.#loadReadStream(); + } + + while (true) { + const buf = stream.read(); + if (buf === null || !this.push(buf)) { + return; + } + } + } + }; + return new StdioWriteStream(fd_); + } + + var { EventEmitter } = require("node:events"); + + function isFastEncoding(encoding) { + if (!encoding) return true; + + var normalied = encoding.toLowerCase(); + return normalied === "utf8" || normalied === "utf-8" || normalied === "buffer" || normalied === "binary"; + } + + var readline; + + var FastStdioWriteStream = class StdioWriteStream extends EventEmitter { + #fd; + #innerStream; + #writer; + #isTTY; + + bytesWritten = 0; + + setDefaultEncoding(encoding) { + if (this.#innerStream || !isFastEncoding(encoding)) { + this.#ensureInnerStream(); + return this.#innerStream.setDefaultEncoding(encoding); + } + } + + #createWriter() { + switch (this.#fd) { + case 1: { + var writer = Bun.stdout.writer({ highWaterMark: 0 }); + writer.unref(); + return writer; + } + + case 2: { + var writer = Bun.stderr.writer({ highWaterMark: 0 }); + writer.unref(); + return writer; + } + default: { + throw new Error("Unsupported writer"); + } + } + } + + #getWriter() { + return (this.#writer ??= this.#createWriter()); + } + + constructor(fd_) { + super(); + this.#fd = fd_; + } + + get fd() { + return this.#fd; + } + + get isTTY() { + return (this.#isTTY ??= require("node:tty").isatty(this.#fd)); + } + + cursorTo(x, y, callback) { + return (readline ??= require("readline")).cursorTo(this, x, y, callback); + } + + moveCursor(dx, dy, callback) { + return (readline ??= require("readline")).moveCursor(this, dx, dy, callback); + } + + clearLine(dir, callback) { + return (readline ??= require("readline")).clearLine(this, dir, callback); + } + + clearScreenDown(callback) { + return (readline ??= require("readline")).clearScreenDown(this, callback); + } + + // TODO: once implemented this.columns and this.rows should be uncommented + // getWindowSize() { + // return [this.columns, this.rows]; + // } + + ref() { + this.#getWriter().ref(); + } + + unref() { + this.#getWriter().unref(); + } + + on(event, listener) { + if (event === "close" || event === "finish") { + this.#ensureInnerStream(); + return this.#innerStream.on(event, listener); + } + + if (event === "drain") { + return super.on("drain", listener); + } + + if (event === "error") { + return super.on("error", listener); + } + + return super.on(event, listener); + } + + get _writableState() { + this.#ensureInnerStream(); + return this.#innerStream._writableState; + } + + get _readableState() { + this.#ensureInnerStream(); + return this.#innerStream._readableState; + } + + pipe(destination) { + this.#ensureInnerStream(); + return this.#innerStream.pipe(destination); + } + + unpipe(destination) { + this.#ensureInnerStream(); + return this.#innerStream.unpipe(destination); + } + + #ensureInnerStream() { + if (this.#innerStream) return; + this.#innerStream = createStdioWriteStream(this.#fd); + const events = this.eventNames(); + for (const event of events) { + this.#innerStream.on(event, (...args) => { + this.emit(event, ...args); + }); + } + } + + #write1(chunk) { + var writer = this.#getWriter(); + const writeResult = writer.write(chunk); + this.bytesWritten += writeResult; + const flushResult = writer.flush(false); + return !!(writeResult || flushResult); + } + + #writeWithEncoding(chunk, encoding) { + if (!isFastEncoding(encoding)) { + this.#ensureInnerStream(); + return this.#innerStream.write(chunk, encoding); + } + + return this.#write1(chunk); + } + + #performCallback(cb, err?: any) { + if (err) { + this.emit("error", err); + } + + try { + cb(err ? err : null); + } catch (err2) { + this.emit("error", err2); + } + } + + #writeWithCallbackAndEncoding(chunk, encoding, callback) { + if (!isFastEncoding(encoding)) { + this.#ensureInnerStream(); + return this.#innerStream.write(chunk, encoding, callback); + } + + var writer = this.#getWriter(); + const writeResult = writer.write(chunk); + const flushResult = writer.flush(true); + if (flushResult?.then) { + flushResult.then( + () => { + this.#performCallback(callback); + this.emit("drain"); + }, + err => this.#performCallback(callback, err), + ); + return false; + } + + queueMicrotask(() => { + this.#performCallback(callback); + }); + + return !!(writeResult || flushResult); + } + + write(chunk, encoding, callback) { + const result = this._write(chunk, encoding, callback); + + if (result) { + this.emit("drain"); + } + + return result; + } + + get hasColors() { + return Bun.tty[this.#fd].hasColors; + } + + _write(chunk, encoding, callback) { + var inner = this.#innerStream; + if (inner) { + return inner.write(chunk, encoding, callback); + } + + switch (arguments.length) { + case 0: { + var error = new Error("Invalid arguments"); + error.code = "ERR_INVALID_ARG_TYPE"; + throw error; + } + case 1: { + return this.#write1(chunk); + } + case 2: { + if (typeof encoding === "function") { + return this.#writeWithCallbackAndEncoding(chunk, "", encoding); + } else if (typeof encoding === "string") { + return this.#writeWithEncoding(chunk, encoding); + } + } + default: { + if ( + (typeof encoding !== "undefined" && typeof encoding !== "string") || + (typeof callback !== "undefined" && typeof callback !== "function") + ) { + var error = new Error("Invalid arguments"); + error.code = "ERR_INVALID_ARG_TYPE"; + throw error; + } + + if (typeof callback === "undefined") { + return this.#writeWithEncoding(chunk, encoding); + } + + return this.#writeWithCallbackAndEncoding(chunk, encoding, callback); + } + } + } + + destroy() { + return this; + } + + end() { + return this; + } + }; + + return new FastStdioWriteStream(fd_); +} + +export function getStdinStream(fd_, rawRequire, Bun) { + var module = { path: "node:process", require: rawRequire }; + var require = path => module.require(path); + + var { Duplex, eos, destroy } = require("node:stream"); + + var StdinStream = class StdinStream extends Duplex { + #reader; + // TODO: investigate https://github.com/oven-sh/bun/issues/1607 + + #readRef; + #writeStream; + + #readable = true; + #unrefOnRead = false; + #writable = true; + + #onFinish; + #onClose; + #onDrain; + + get isTTY() { + return require("tty").isatty(fd_); + } + + get fd() { + return fd_; + } + + constructor() { + super({ readable: true, writable: true }); + } + + #onFinished(err?) { + const cb = this.#onClose; + this.#onClose = null; + + if (cb) { + cb(err); + } else if (err) { + this.destroy(err); + } else if (!this.#readable && !this.#writable) { + this.destroy(); + } + } + + _destroy(err, callback) { + if (!err && this.#onClose !== null) { + var AbortError = class AbortError extends Error { + constructor(message = "The operation was aborted", options = void 0) { + if (options !== void 0 && typeof options !== "object") { + throw new Error(`Invalid AbortError options:\n\n${JSON.stringify(options, null, 2)}`); + } + super(message, options); + this.code = "ABORT_ERR"; + this.name = "AbortError"; + } + }; + err = new AbortError(); + } + + if (this.#onClose === null) { + callback(err); + } else { + this.#onClose = callback; + if (this.#writeStream) destroy(this.#writeStream, err); + } + } + + setRawMode(mode) {} + + on(name, callback) { + // Streams don't generally required to present any data when only + // `readable` events are present, i.e. `readableFlowing === false` + // + // However, Node.js has a this quirk whereby `process.stdin.read()` + // blocks under TTY mode, thus looping `.read()` in this particular + // case would not result in truncation. + // + // Therefore the following hack is only specific to `process.stdin` + // and does not apply to the underlying Stream implementation. + if (name === "readable") { + this.ref(); + this.#unrefOnRead = true; + } + return super.on(name, callback); + } + + pause() { + this.unref(); + return super.pause(); + } + + resume() { + this.ref(); + return super.resume(); + } + + ref() { + this.#reader ??= Bun.stdin.stream().getReader(); + this.#readRef ??= setInterval(() => {}, 1 << 30); + } + + unref() { + if (this.#readRef) { + clearInterval(this.#readRef); + this.#readRef = null; + } + } + + async #readInternal() { + try { + var done, value; + const read = this.#reader.readMany(); + + // read same-tick if possible + if (!read?.then) { + ({ done, value } = read); + } else { + ({ done, value } = await read); + } + + if (!done) { + this.push(value[0]); + + // shouldn't actually happen, but just in case + const length = value.length; + for (let i = 1; i < length; i++) { + this.push(value[i]); + } + } else { + this.push(null); + this.pause(); + this.#readable = false; + this.#onFinished(); + } + } catch (err) { + this.#readable = false; + this.#onFinished(err); + } + } + + _read(size) { + if (this.#unrefOnRead) { + this.unref(); + this.#unrefOnRead = false; + } + this.#readInternal(); + } + + #constructWriteStream() { + var { createWriteStream } = require("node:fs"); + var writeStream = (this.#writeStream = createWriteStream("/dev/fd/0")); + + writeStream.on("finish", () => { + if (this.#onFinish) { + const cb = this.#onFinish; + this.#onFinish = null; + cb(); + } + }); + + writeStream.on("drain", () => { + if (this.#onDrain) { + const cb = this.#onDrain; + this.#onDrain = null; + cb(); + } + }); + + eos(writeStream, err => { + this.#writable = false; + if (err) { + destroy(writeStream, err); + } + this.#onFinished(err); + }); + + return writeStream; + } + + _write(chunk, encoding, callback) { + var writeStream = this.#writeStream; + if (!writeStream) { + writeStream = this.#constructWriteStream(); + } + + if (writeStream.write(chunk, encoding)) { + callback(); + } else { + this.#onDrain = callback; + } + } + + _final(callback) { + this.#writeStream.end(); + this.#onFinish = (...args) => callback(...args); + } + }; + + return new StdinStream(); +} diff --git a/src/js/builtins/README.md b/src/js/builtins/README.md new file mode 100644 index 000000000..67b8882ee --- /dev/null +++ b/src/js/builtins/README.md @@ -0,0 +1,53 @@ +# JavaScript Builtins + +**TLDR** — When files in this directory change, run: + +```bash +# Delete the built files +$ make regenerate-bindings +# Re-link the binary without compiling zig (so it's faster) +$ make bun-link-lld-debug +``` + +TypeScript files in [./ts](./ts) are bundled into C++ Headers that can access JavaScriptCore intrinsics. These files use special globals that are prefixed with `$`. + +```js +$getter +export function foo() { + return $getByIdDirectPrivate(this, "superSecret"); +} +``` + +It looks kind of like decorators but they're not. They let you directly call engine intrinsics and help with avoiding prototype pollution issues. + +V8 has a [similar feature](https://v8.dev/blog/embedded-builtins) (they use `%` instead of `@`) + +They usually are accompanied by a C++ file. + +We use a custom code generator located in `./codegen` which contains a regex-based parser that separates each function into it's own bundling context, so syntax like top level variables / functions will not work. + +You can also use `process.platform` and `process.arch` in these files. The values are inlined and DCE'd. + +## Generating builtins + +To regenerate the builtins, run this from Bun's project root (where the `Makefile` is) + +```bash +$ make builtins +``` + +You'll want to also rebuild all the C++ bindings or you will get strange crashes on start + +```bash +$ make clean-bindings +``` + +The `make regenerate-bindings` command will clean and rebuild the bindings. + +Also, you can run the code generator manually. + +```bash +$ bun ./codegen/index.ts +# pass --minify to minify (make passes this by default) +# pass --keep-tmp to keep the temporary ./tmp folder, which contains processed pre-bundled .ts files +``` diff --git a/src/js/builtins/ReadableByteStreamController.ts b/src/js/builtins/ReadableByteStreamController.ts new file mode 100644 index 000000000..888f241bc --- /dev/null +++ b/src/js/builtins/ReadableByteStreamController.ts @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2016 Canon Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeReadableByteStreamController(this, stream, underlyingByteSource, highWaterMark) { + if (arguments.length !== 4 && arguments[3] !== $isReadableStream) + throw new TypeError("ReadableByteStreamController constructor should not be called directly"); + + return $privateInitializeReadableByteStreamController.$call(this, stream, underlyingByteSource, highWaterMark); +} + +export function enqueue(this, chunk) { + if (!$isReadableByteStreamController(this)) throw $makeThisTypeError("ReadableByteStreamController", "enqueue"); + + if ($getByIdDirectPrivate(this, "closeRequested")) + throw new TypeError("ReadableByteStreamController is requested to close"); + + if ($getByIdDirectPrivate($getByIdDirectPrivate(this, "controlledReadableStream"), "state") !== $streamReadable) + throw new TypeError("ReadableStream is not readable"); + + if (!$isObject(chunk) || !ArrayBuffer.$isView(chunk)) throw new TypeError("Provided chunk is not a TypedArray"); + + return $readableByteStreamControllerEnqueue(this, chunk); +} + +export function error(this, error) { + if (!$isReadableByteStreamController(this)) throw $makeThisTypeError("ReadableByteStreamController", "error"); + + if ($getByIdDirectPrivate($getByIdDirectPrivate(this, "controlledReadableStream"), "state") !== $streamReadable) + throw new TypeError("ReadableStream is not readable"); + + $readableByteStreamControllerError(this, error); +} + +export function close(this) { + if (!$isReadableByteStreamController(this)) throw $makeThisTypeError("ReadableByteStreamController", "close"); + + if ($getByIdDirectPrivate(this, "closeRequested")) throw new TypeError("Close has already been requested"); + + if ($getByIdDirectPrivate($getByIdDirectPrivate(this, "controlledReadableStream"), "state") !== $streamReadable) + throw new TypeError("ReadableStream is not readable"); + + $readableByteStreamControllerClose(this); +} + +$getter; +export function byobRequest(this) { + if (!$isReadableByteStreamController(this)) throw $makeGetterTypeError("ReadableByteStreamController", "byobRequest"); + + var request = $getByIdDirectPrivate(this, "byobRequest"); + if (request === undefined) { + var pending = $getByIdDirectPrivate(this, "pendingPullIntos"); + const firstDescriptor = pending.peek(); + if (firstDescriptor) { + const view = new Uint8Array( + firstDescriptor.buffer, + firstDescriptor.byteOffset + firstDescriptor.bytesFilled, + firstDescriptor.byteLength - firstDescriptor.bytesFilled, + ); + $putByIdDirectPrivate(this, "byobRequest", new ReadableStreamBYOBRequest(this, view, $isReadableStream)); + } + } + + return $getByIdDirectPrivate(this, "byobRequest"); +} + +$getter; +export function desiredSize(this) { + if (!$isReadableByteStreamController(this)) throw $makeGetterTypeError("ReadableByteStreamController", "desiredSize"); + + return $readableByteStreamControllerGetDesiredSize(this); +} diff --git a/src/js/builtins/ReadableByteStreamInternals.ts b/src/js/builtins/ReadableByteStreamInternals.ts new file mode 100644 index 000000000..f44c385b4 --- /dev/null +++ b/src/js/builtins/ReadableByteStreamInternals.ts @@ -0,0 +1,656 @@ +/* + * Copyright (C) 2016 Canon Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +// @internal + +export function privateInitializeReadableByteStreamController(this, stream, underlyingByteSource, highWaterMark) { + if (!$isReadableStream(stream)) throw new TypeError("ReadableByteStreamController needs a ReadableStream"); + + // readableStreamController is initialized with null value. + if ($getByIdDirectPrivate(stream, "readableStreamController") !== null) + throw new TypeError("ReadableStream already has a controller"); + + $putByIdDirectPrivate(this, "controlledReadableStream", stream); + $putByIdDirectPrivate(this, "underlyingByteSource", underlyingByteSource); + $putByIdDirectPrivate(this, "pullAgain", false); + $putByIdDirectPrivate(this, "pulling", false); + $readableByteStreamControllerClearPendingPullIntos(this); + $putByIdDirectPrivate(this, "queue", $newQueue()); + $putByIdDirectPrivate(this, "started", 0); + $putByIdDirectPrivate(this, "closeRequested", false); + + let hwm = $toNumber(highWaterMark); + if (isNaN(hwm) || hwm < 0) throw new RangeError("highWaterMark value is negative or not a number"); + $putByIdDirectPrivate(this, "strategyHWM", hwm); + + let autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize; + if (autoAllocateChunkSize !== undefined) { + autoAllocateChunkSize = $toNumber(autoAllocateChunkSize); + if (autoAllocateChunkSize <= 0 || autoAllocateChunkSize === Infinity || autoAllocateChunkSize === -Infinity) + throw new RangeError("autoAllocateChunkSize value is negative or equal to positive or negative infinity"); + } + $putByIdDirectPrivate(this, "autoAllocateChunkSize", autoAllocateChunkSize); + $putByIdDirectPrivate(this, "pendingPullIntos", $createFIFO()); + + const controller = this; + $promiseInvokeOrNoopNoCatch($getByIdDirectPrivate(controller, "underlyingByteSource"), "start", [controller]).$then( + () => { + $putByIdDirectPrivate(controller, "started", 1); + $assert(!$getByIdDirectPrivate(controller, "pulling")); + $assert(!$getByIdDirectPrivate(controller, "pullAgain")); + $readableByteStreamControllerCallPullIfNeeded(controller); + }, + error => { + if ($getByIdDirectPrivate(stream, "state") === $streamReadable) + $readableByteStreamControllerError(controller, error); + }, + ); + + $putByIdDirectPrivate(this, "cancel", $readableByteStreamControllerCancel); + $putByIdDirectPrivate(this, "pull", $readableByteStreamControllerPull); + + return this; +} + +export function readableStreamByteStreamControllerStart(this, controller) { + $putByIdDirectPrivate(controller, "start", undefined); +} + +export function privateInitializeReadableStreamBYOBRequest(this, controller, view) { + $putByIdDirectPrivate(this, "associatedReadableByteStreamController", controller); + $putByIdDirectPrivate(this, "view", view); +} + +export function isReadableByteStreamController(controller) { + // Same test mechanism as in isReadableStreamDefaultController (ReadableStreamInternals.js). + // See corresponding function for explanations. + return $isObject(controller) && !!$getByIdDirectPrivate(controller, "underlyingByteSource"); +} + +export function isReadableStreamBYOBRequest(byobRequest) { + // Same test mechanism as in isReadableStreamDefaultController (ReadableStreamInternals.js). + // See corresponding function for explanations. + return $isObject(byobRequest) && !!$getByIdDirectPrivate(byobRequest, "associatedReadableByteStreamController"); +} + +export function isReadableStreamBYOBReader(reader) { + // Spec tells to return true only if reader has a readIntoRequests internal slot. + // However, since it is a private slot, it cannot be checked using hasOwnProperty(). + // Since readIntoRequests is initialized with an empty array, the following test is ok. + return $isObject(reader) && !!$getByIdDirectPrivate(reader, "readIntoRequests"); +} + +export function readableByteStreamControllerCancel(controller, reason) { + var pendingPullIntos = $getByIdDirectPrivate(controller, "pendingPullIntos"); + var first = pendingPullIntos.peek(); + if (first) first.bytesFilled = 0; + + $putByIdDirectPrivate(controller, "queue", $newQueue()); + return $promiseInvokeOrNoop($getByIdDirectPrivate(controller, "underlyingByteSource"), "cancel", [reason]); +} + +export function readableByteStreamControllerError(controller, e) { + $assert( + $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === $streamReadable, + ); + $readableByteStreamControllerClearPendingPullIntos(controller); + $putByIdDirectPrivate(controller, "queue", $newQueue()); + $readableStreamError($getByIdDirectPrivate(controller, "controlledReadableStream"), e); +} + +export function readableByteStreamControllerClose(controller) { + $assert(!$getByIdDirectPrivate(controller, "closeRequested")); + $assert( + $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === $streamReadable, + ); + + if ($getByIdDirectPrivate(controller, "queue").size > 0) { + $putByIdDirectPrivate(controller, "closeRequested", true); + return; + } + + var first = $getByIdDirectPrivate(controller, "pendingPullIntos")?.peek(); + if (first) { + if (first.bytesFilled > 0) { + const e = $makeTypeError("Close requested while there remain pending bytes"); + $readableByteStreamControllerError(controller, e); + throw e; + } + } + + $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); +} + +export function readableByteStreamControllerClearPendingPullIntos(controller) { + $readableByteStreamControllerInvalidateBYOBRequest(controller); + var existing = $getByIdDirectPrivate(controller, "pendingPullIntos"); + if (existing !== undefined) { + existing.clear(); + } else { + $putByIdDirectPrivate(controller, "pendingPullIntos", $createFIFO()); + } +} + +export function readableByteStreamControllerGetDesiredSize(controller) { + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + const state = $getByIdDirectPrivate(stream, "state"); + + if (state === $streamErrored) return null; + if (state === $streamClosed) return 0; + + return $getByIdDirectPrivate(controller, "strategyHWM") - $getByIdDirectPrivate(controller, "queue").size; +} + +export function readableStreamHasBYOBReader(stream) { + const reader = $getByIdDirectPrivate(stream, "reader"); + return reader !== undefined && $isReadableStreamBYOBReader(reader); +} + +export function readableStreamHasDefaultReader(stream) { + const reader = $getByIdDirectPrivate(stream, "reader"); + return reader !== undefined && $isReadableStreamDefaultReader(reader); +} + +export function readableByteStreamControllerHandleQueueDrain(controller) { + $assert( + $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === $streamReadable, + ); + if (!$getByIdDirectPrivate(controller, "queue").size && $getByIdDirectPrivate(controller, "closeRequested")) + $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); + else $readableByteStreamControllerCallPullIfNeeded(controller); +} + +export function readableByteStreamControllerPull(controller) { + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + $assert($readableStreamHasDefaultReader(stream)); + if ($getByIdDirectPrivate(controller, "queue").content?.isNotEmpty()) { + const entry = $getByIdDirectPrivate(controller, "queue").content.shift(); + $getByIdDirectPrivate(controller, "queue").size -= entry.byteLength; + $readableByteStreamControllerHandleQueueDrain(controller); + let view; + try { + view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength); + } catch (error) { + return Promise.$reject(error); + } + return $createFulfilledPromise({ value: view, done: false }); + } + + if ($getByIdDirectPrivate(controller, "autoAllocateChunkSize") !== undefined) { + let buffer; + try { + buffer = $createUninitializedArrayBuffer($getByIdDirectPrivate(controller, "autoAllocateChunkSize")); + } catch (error) { + return Promise.$reject(error); + } + const pullIntoDescriptor = { + buffer, + byteOffset: 0, + byteLength: $getByIdDirectPrivate(controller, "autoAllocateChunkSize"), + bytesFilled: 0, + elementSize: 1, + ctor: Uint8Array, + readerType: "default", + }; + $getByIdDirectPrivate(controller, "pendingPullIntos").push(pullIntoDescriptor); + } + + const promise = $readableStreamAddReadRequest(stream); + $readableByteStreamControllerCallPullIfNeeded(controller); + return promise; +} + +export function readableByteStreamControllerShouldCallPull(controller) { + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + + if ($getByIdDirectPrivate(stream, "state") !== $streamReadable) return false; + if ($getByIdDirectPrivate(controller, "closeRequested")) return false; + if (!($getByIdDirectPrivate(controller, "started") > 0)) return false; + const reader = $getByIdDirectPrivate(stream, "reader"); + + if ( + reader && + ($getByIdDirectPrivate(reader, "readRequests")?.isNotEmpty() || !!$getByIdDirectPrivate(reader, "bunNativePtr")) + ) + return true; + if ( + $readableStreamHasBYOBReader(stream) && + $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readIntoRequests")?.isNotEmpty() + ) + return true; + if ($readableByteStreamControllerGetDesiredSize(controller) > 0) return true; + return false; +} + +export function readableByteStreamControllerCallPullIfNeeded(controller) { + if (!$readableByteStreamControllerShouldCallPull(controller)) return; + + if ($getByIdDirectPrivate(controller, "pulling")) { + $putByIdDirectPrivate(controller, "pullAgain", true); + return; + } + + $assert(!$getByIdDirectPrivate(controller, "pullAgain")); + $putByIdDirectPrivate(controller, "pulling", true); + $promiseInvokeOrNoop($getByIdDirectPrivate(controller, "underlyingByteSource"), "pull", [controller]).$then( + () => { + $putByIdDirectPrivate(controller, "pulling", false); + if ($getByIdDirectPrivate(controller, "pullAgain")) { + $putByIdDirectPrivate(controller, "pullAgain", false); + $readableByteStreamControllerCallPullIfNeeded(controller); + } + }, + error => { + if ( + $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === + $streamReadable + ) + $readableByteStreamControllerError(controller, error); + }, + ); +} + +export function transferBufferToCurrentRealm(buffer) { + // FIXME: Determine what should be done here exactly (what is already existing in current + // codebase and what has to be added). According to spec, Transfer operation should be + // performed in order to transfer buffer to current realm. For the moment, simply return + // received buffer. + return buffer; +} + +export function readableStreamReaderKind(reader) { + if (!!$getByIdDirectPrivate(reader, "readRequests")) return $getByIdDirectPrivate(reader, "bunNativePtr") ? 3 : 1; + + if (!!$getByIdDirectPrivate(reader, "readIntoRequests")) return 2; + + return 0; +} + +export function readableByteStreamControllerEnqueue(controller, chunk) { + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + $assert(!$getByIdDirectPrivate(controller, "closeRequested")); + $assert($getByIdDirectPrivate(stream, "state") === $streamReadable); + + switch ( + $getByIdDirectPrivate(stream, "reader") ? $readableStreamReaderKind($getByIdDirectPrivate(stream, "reader")) : 0 + ) { + /* default reader */ + case 1: { + if (!$getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests")?.isNotEmpty()) + $readableByteStreamControllerEnqueueChunk( + controller, + $transferBufferToCurrentRealm(chunk.buffer), + chunk.byteOffset, + chunk.byteLength, + ); + else { + $assert(!$getByIdDirectPrivate(controller, "queue").content.size()); + const transferredView = + chunk.constructor === Uint8Array ? chunk : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); + $readableStreamFulfillReadRequest(stream, transferredView, false); + } + break; + } + + /* BYOB */ + case 2: { + $readableByteStreamControllerEnqueueChunk( + controller, + $transferBufferToCurrentRealm(chunk.buffer), + chunk.byteOffset, + chunk.byteLength, + ); + $readableByteStreamControllerProcessPullDescriptors(controller); + break; + } + + /* NativeReader */ + case 3: { + // reader.$enqueueNative($getByIdDirectPrivate(reader, "bunNativePtr"), chunk); + + break; + } + + default: { + $assert(!$isReadableStreamLocked(stream)); + $readableByteStreamControllerEnqueueChunk( + controller, + $transferBufferToCurrentRealm(chunk.buffer), + chunk.byteOffset, + chunk.byteLength, + ); + break; + } + } +} + +// Spec name: readableByteStreamControllerEnqueueChunkToQueue. +export function readableByteStreamControllerEnqueueChunk(controller, buffer, byteOffset, byteLength) { + $getByIdDirectPrivate(controller, "queue").content.push({ + buffer: buffer, + byteOffset: byteOffset, + byteLength: byteLength, + }); + $getByIdDirectPrivate(controller, "queue").size += byteLength; +} + +export function readableByteStreamControllerRespondWithNewView(controller, view) { + $assert($getByIdDirectPrivate(controller, "pendingPullIntos").isNotEmpty()); + + let firstDescriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); + + if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) + throw new RangeError("Invalid value for view.byteOffset"); + + if (firstDescriptor.byteLength !== view.byteLength) throw new RangeError("Invalid value for view.byteLength"); + + firstDescriptor.buffer = view.buffer; + $readableByteStreamControllerRespondInternal(controller, view.byteLength); +} + +export function readableByteStreamControllerRespond(controller, bytesWritten) { + bytesWritten = $toNumber(bytesWritten); + + if (isNaN(bytesWritten) || bytesWritten === Infinity || bytesWritten < 0) + throw new RangeError("bytesWritten has an incorrect value"); + + $assert($getByIdDirectPrivate(controller, "pendingPullIntos").isNotEmpty()); + + $readableByteStreamControllerRespondInternal(controller, bytesWritten); +} + +export function readableByteStreamControllerRespondInternal(controller, bytesWritten) { + let firstDescriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); + let stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + + if ($getByIdDirectPrivate(stream, "state") === $streamClosed) { + if (bytesWritten !== 0) throw new TypeError("bytesWritten is different from 0 even though stream is closed"); + $readableByteStreamControllerRespondInClosedState(controller, firstDescriptor); + } else { + $assert($getByIdDirectPrivate(stream, "state") === $streamReadable); + $readableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor); + } +} + +export function readableByteStreamControllerRespondInReadableState(controller, bytesWritten, pullIntoDescriptor) { + if (pullIntoDescriptor.bytesFilled + bytesWritten > pullIntoDescriptor.byteLength) + throw new RangeError("bytesWritten value is too great"); + + $assert( + $getByIdDirectPrivate(controller, "pendingPullIntos").isEmpty() || + $getByIdDirectPrivate(controller, "pendingPullIntos").peek() === pullIntoDescriptor, + ); + $readableByteStreamControllerInvalidateBYOBRequest(controller); + pullIntoDescriptor.bytesFilled += bytesWritten; + + if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize) return; + + $readableByteStreamControllerShiftPendingDescriptor(controller); + const remainderSize = pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize; + + if (remainderSize > 0) { + const end = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; + const remainder = $cloneArrayBuffer(pullIntoDescriptor.buffer, end - remainderSize, remainderSize); + $readableByteStreamControllerEnqueueChunk(controller, remainder, 0, remainder.byteLength); + } + + pullIntoDescriptor.buffer = $transferBufferToCurrentRealm(pullIntoDescriptor.buffer); + pullIntoDescriptor.bytesFilled -= remainderSize; + $readableByteStreamControllerCommitDescriptor( + $getByIdDirectPrivate(controller, "controlledReadableStream"), + pullIntoDescriptor, + ); + $readableByteStreamControllerProcessPullDescriptors(controller); +} + +export function readableByteStreamControllerRespondInClosedState(controller, firstDescriptor) { + firstDescriptor.buffer = $transferBufferToCurrentRealm(firstDescriptor.buffer); + $assert(firstDescriptor.bytesFilled === 0); + + if ($readableStreamHasBYOBReader($getByIdDirectPrivate(controller, "controlledReadableStream"))) { + while ( + $getByIdDirectPrivate( + $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "reader"), + "readIntoRequests", + )?.isNotEmpty() + ) { + let pullIntoDescriptor = $readableByteStreamControllerShiftPendingDescriptor(controller); + $readableByteStreamControllerCommitDescriptor( + $getByIdDirectPrivate(controller, "controlledReadableStream"), + pullIntoDescriptor, + ); + } + } +} + +// Spec name: readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue (shortened for readability). +export function readableByteStreamControllerProcessPullDescriptors(controller) { + $assert(!$getByIdDirectPrivate(controller, "closeRequested")); + while ($getByIdDirectPrivate(controller, "pendingPullIntos").isNotEmpty()) { + if ($getByIdDirectPrivate(controller, "queue").size === 0) return; + let pullIntoDescriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); + if ($readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor)) { + $readableByteStreamControllerShiftPendingDescriptor(controller); + $readableByteStreamControllerCommitDescriptor( + $getByIdDirectPrivate(controller, "controlledReadableStream"), + pullIntoDescriptor, + ); + } + } +} + +// Spec name: readableByteStreamControllerFillPullIntoDescriptorFromQueue (shortened for readability). +export function readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor) { + const currentAlignedBytes = + pullIntoDescriptor.bytesFilled - (pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize); + const maxBytesToCopy = + $getByIdDirectPrivate(controller, "queue").size < pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled + ? $getByIdDirectPrivate(controller, "queue").size + : pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled; + const maxBytesFilled = pullIntoDescriptor.bytesFilled + maxBytesToCopy; + const maxAlignedBytes = maxBytesFilled - (maxBytesFilled % pullIntoDescriptor.elementSize); + let totalBytesToCopyRemaining = maxBytesToCopy; + let ready = false; + + if (maxAlignedBytes > currentAlignedBytes) { + totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor.bytesFilled; + ready = true; + } + + while (totalBytesToCopyRemaining > 0) { + let headOfQueue = $getByIdDirectPrivate(controller, "queue").content.peek(); + const bytesToCopy = + totalBytesToCopyRemaining < headOfQueue.byteLength ? totalBytesToCopyRemaining : headOfQueue.byteLength; + // Copy appropriate part of pullIntoDescriptor.buffer to headOfQueue.buffer. + // Remark: this implementation is not completely aligned on the definition of CopyDataBlockBytes + // operation of ECMAScript (the case of Shared Data Block is not considered here, but it doesn't seem to be an issue). + const destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; + // FIXME: As indicated in comments of bug 172717, access to set is not safe. However, using prototype.$set.$call does + // not work ($set is undefined). A safe way to do that is needed. + new Uint8Array(pullIntoDescriptor.buffer).set( + new Uint8Array(headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy), + destStart, + ); + + if (headOfQueue.byteLength === bytesToCopy) $getByIdDirectPrivate(controller, "queue").content.shift(); + else { + headOfQueue.byteOffset += bytesToCopy; + headOfQueue.byteLength -= bytesToCopy; + } + + $getByIdDirectPrivate(controller, "queue").size -= bytesToCopy; + $assert( + $getByIdDirectPrivate(controller, "pendingPullIntos").isEmpty() || + $getByIdDirectPrivate(controller, "pendingPullIntos").peek() === pullIntoDescriptor, + ); + $readableByteStreamControllerInvalidateBYOBRequest(controller); + pullIntoDescriptor.bytesFilled += bytesToCopy; + totalBytesToCopyRemaining -= bytesToCopy; + } + + if (!ready) { + $assert($getByIdDirectPrivate(controller, "queue").size === 0); + $assert(pullIntoDescriptor.bytesFilled > 0); + $assert(pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize); + } + + return ready; +} + +// Spec name: readableByteStreamControllerShiftPendingPullInto (renamed for consistency). +export function readableByteStreamControllerShiftPendingDescriptor(controller) { + let descriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").shift(); + $readableByteStreamControllerInvalidateBYOBRequest(controller); + return descriptor; +} + +export function readableByteStreamControllerInvalidateBYOBRequest(controller) { + if ($getByIdDirectPrivate(controller, "byobRequest") === undefined) return; + const byobRequest = $getByIdDirectPrivate(controller, "byobRequest"); + $putByIdDirectPrivate(byobRequest, "associatedReadableByteStreamController", undefined); + $putByIdDirectPrivate(byobRequest, "view", undefined); + $putByIdDirectPrivate(controller, "byobRequest", undefined); +} + +// Spec name: readableByteStreamControllerCommitPullIntoDescriptor (shortened for readability). +export function readableByteStreamControllerCommitDescriptor(stream, pullIntoDescriptor) { + $assert($getByIdDirectPrivate(stream, "state") !== $streamErrored); + let done = false; + if ($getByIdDirectPrivate(stream, "state") === $streamClosed) { + $assert(!pullIntoDescriptor.bytesFilled); + done = true; + } + let filledView = $readableByteStreamControllerConvertDescriptor(pullIntoDescriptor); + if (pullIntoDescriptor.readerType === "default") $readableStreamFulfillReadRequest(stream, filledView, done); + else { + $assert(pullIntoDescriptor.readerType === "byob"); + $readableStreamFulfillReadIntoRequest(stream, filledView, done); + } +} + +// Spec name: readableByteStreamControllerConvertPullIntoDescriptor (shortened for readability). +export function readableByteStreamControllerConvertDescriptor(pullIntoDescriptor) { + $assert(pullIntoDescriptor.bytesFilled <= pullIntoDescriptor.byteLength); + $assert(pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize === 0); + + return new pullIntoDescriptor.ctor( + pullIntoDescriptor.buffer, + pullIntoDescriptor.byteOffset, + pullIntoDescriptor.bytesFilled / pullIntoDescriptor.elementSize, + ); +} + +export function readableStreamFulfillReadIntoRequest(stream, chunk, done) { + const readIntoRequest = $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readIntoRequests").shift(); + $fulfillPromise(readIntoRequest, { value: chunk, done: done }); +} + +export function readableStreamBYOBReaderRead(reader, view) { + const stream = $getByIdDirectPrivate(reader, "ownerReadableStream"); + $assert(!!stream); + + $putByIdDirectPrivate(stream, "disturbed", true); + if ($getByIdDirectPrivate(stream, "state") === $streamErrored) + return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); + + return $readableByteStreamControllerPullInto($getByIdDirectPrivate(stream, "readableStreamController"), view); +} + +export function readableByteStreamControllerPullInto(controller, view) { + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + let elementSize = 1; + // Spec describes that in the case where view is a TypedArray, elementSize + // should be set to the size of an element (e.g. 2 for UInt16Array). For + // DataView, BYTES_PER_ELEMENT is undefined, contrary to the same property + // for TypedArrays. + // FIXME: Getting BYTES_PER_ELEMENT like this is not safe (property is read-only + // but can be modified if the prototype is redefined). A safe way of getting + // it would be to determine which type of ArrayBufferView view is an instance + // of based on typed arrays private variables. However, this is not possible due + // to bug 167697, which prevents access to typed arrays through their private + // names unless public name has already been met before. + if (view.BYTES_PER_ELEMENT !== undefined) elementSize = view.BYTES_PER_ELEMENT; + + // FIXME: Getting constructor like this is not safe. A safe way of getting + // it would be to determine which type of ArrayBufferView view is an instance + // of, and to assign appropriate constructor based on this (e.g. ctor = + // $Uint8Array). However, this is not possible due to bug 167697, which + // prevents access to typed arrays through their private names unless public + // name has already been met before. + const ctor = view.constructor; + + const pullIntoDescriptor = { + buffer: view.buffer, + byteOffset: view.byteOffset, + byteLength: view.byteLength, + bytesFilled: 0, + elementSize, + ctor, + readerType: "byob", + }; + + var pending = $getByIdDirectPrivate(controller, "pendingPullIntos"); + if (pending?.isNotEmpty()) { + pullIntoDescriptor.buffer = $transferBufferToCurrentRealm(pullIntoDescriptor.buffer); + pending.push(pullIntoDescriptor); + return $readableStreamAddReadIntoRequest(stream); + } + + if ($getByIdDirectPrivate(stream, "state") === $streamClosed) { + const emptyView = new ctor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, 0); + return $createFulfilledPromise({ value: emptyView, done: true }); + } + + if ($getByIdDirectPrivate(controller, "queue").size > 0) { + if ($readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor)) { + const filledView = $readableByteStreamControllerConvertDescriptor(pullIntoDescriptor); + $readableByteStreamControllerHandleQueueDrain(controller); + return $createFulfilledPromise({ value: filledView, done: false }); + } + if ($getByIdDirectPrivate(controller, "closeRequested")) { + const e = $makeTypeError("Closing stream has been requested"); + $readableByteStreamControllerError(controller, e); + return Promise.$reject(e); + } + } + + pullIntoDescriptor.buffer = $transferBufferToCurrentRealm(pullIntoDescriptor.buffer); + $getByIdDirectPrivate(controller, "pendingPullIntos").push(pullIntoDescriptor); + const promise = $readableStreamAddReadIntoRequest(stream); + $readableByteStreamControllerCallPullIfNeeded(controller); + return promise; +} + +export function readableStreamAddReadIntoRequest(stream) { + $assert($isReadableStreamBYOBReader($getByIdDirectPrivate(stream, "reader"))); + $assert( + $getByIdDirectPrivate(stream, "state") === $streamReadable || + $getByIdDirectPrivate(stream, "state") === $streamClosed, + ); + + const readRequest = $newPromise(); + $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readIntoRequests").push(readRequest); + + return readRequest; +} diff --git a/src/js/builtins/ReadableStream.ts b/src/js/builtins/ReadableStream.ts new file mode 100644 index 000000000..613f869e5 --- /dev/null +++ b/src/js/builtins/ReadableStream.ts @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2015 Canon Inc. + * Copyright (C) 2015 Igalia. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeReadableStream(this: any, underlyingSource: UnderlyingSource, strategy: any) { + if (underlyingSource === undefined) + underlyingSource = { $bunNativeType: 0, $bunNativePtr: 0, $lazy: false } as UnderlyingSource; + if (strategy === undefined) strategy = {}; + + if (!$isObject(underlyingSource)) throw new TypeError("ReadableStream constructor takes an object as first argument"); + + if (strategy !== undefined && !$isObject(strategy)) + throw new TypeError("ReadableStream constructor takes an object as second argument, if any"); + + $putByIdDirectPrivate(this, "state", $streamReadable); + + $putByIdDirectPrivate(this, "reader", undefined); + + $putByIdDirectPrivate(this, "storedError", undefined); + + $putByIdDirectPrivate(this, "disturbed", false); + + // Initialized with null value to enable distinction with undefined case. + $putByIdDirectPrivate(this, "readableStreamController", null); + $putByIdDirectPrivate(this, "bunNativeType", $getByIdDirectPrivate(underlyingSource, "bunNativeType") ?? 0); + $putByIdDirectPrivate(this, "bunNativePtr", $getByIdDirectPrivate(underlyingSource, "bunNativePtr") ?? 0); + + const isDirect = underlyingSource.type === "direct"; + // direct streams are always lazy + const isUnderlyingSourceLazy = !!underlyingSource.$lazy; + const isLazy = isDirect || isUnderlyingSourceLazy; + + // FIXME: We should introduce https://streams.spec.whatwg.org/#create-readable-stream. + // For now, we emulate this with underlyingSource with private properties. + if ($getByIdDirectPrivate(underlyingSource, "pull") !== undefined && !isLazy) { + const size = $getByIdDirectPrivate(strategy, "size"); + const highWaterMark = $getByIdDirectPrivate(strategy, "highWaterMark"); + $putByIdDirectPrivate(this, "highWaterMark", highWaterMark); + $putByIdDirectPrivate(this, "underlyingSource", undefined); + $setupReadableStreamDefaultController( + this, + underlyingSource, + size, + highWaterMark !== undefined ? highWaterMark : 1, + $getByIdDirectPrivate(underlyingSource, "start"), + $getByIdDirectPrivate(underlyingSource, "pull"), + $getByIdDirectPrivate(underlyingSource, "cancel"), + ); + + return this; + } + if (isDirect) { + $putByIdDirectPrivate(this, "underlyingSource", underlyingSource); + $putByIdDirectPrivate(this, "highWaterMark", $getByIdDirectPrivate(strategy, "highWaterMark")); + $putByIdDirectPrivate(this, "start", () => $createReadableStreamController(this, underlyingSource, strategy)); + } else if (isLazy) { + const autoAllocateChunkSize = underlyingSource.autoAllocateChunkSize; + $putByIdDirectPrivate(this, "highWaterMark", undefined); + $putByIdDirectPrivate(this, "underlyingSource", undefined); + $putByIdDirectPrivate( + this, + "highWaterMark", + autoAllocateChunkSize || $getByIdDirectPrivate(strategy, "highWaterMark"), + ); + + $putByIdDirectPrivate(this, "start", () => { + const instance = $lazyLoadStream(this, autoAllocateChunkSize); + if (instance) { + $createReadableStreamController(this, instance, strategy); + } + }); + } else { + $putByIdDirectPrivate(this, "underlyingSource", undefined); + $putByIdDirectPrivate(this, "highWaterMark", $getByIdDirectPrivate(strategy, "highWaterMark")); + $putByIdDirectPrivate(this, "start", undefined); + $createReadableStreamController(this, underlyingSource, strategy); + } + + return this; +} + +$linkTimeConstant; +export function readableStreamToArray(stream: ReadableStream): Promise<unknown[]> { + // this is a direct stream + var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); + if (underlyingSource !== undefined) { + return $readableStreamToArrayDirect(stream, underlyingSource); + } + + return $readableStreamIntoArray(stream); +} + +$linkTimeConstant; +export function readableStreamToText(stream: ReadableStream): Promise<string> { + // this is a direct stream + var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); + if (underlyingSource !== undefined) { + return $readableStreamToTextDirect(stream, underlyingSource); + } + + return $readableStreamIntoText(stream); +} + +$linkTimeConstant; +export function readableStreamToArrayBuffer(stream: ReadableStream<ArrayBuffer>): Promise<ArrayBuffer> | ArrayBuffer { + // this is a direct stream + var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); + + if (underlyingSource !== undefined) { + return $readableStreamToArrayBufferDirect(stream, underlyingSource); + } + + var array = Bun.readableStreamToArray(stream); + if ($isPromise(array)) { + return array.$then(Bun.concatArrayBuffers); + } + + return Bun.concatArrayBuffers(array); +} + +$linkTimeConstant; +export function readableStreamToJSON(stream: ReadableStream): unknown { + return Bun.readableStreamToText(stream).$then(globalThis.JSON.parse); +} + +$linkTimeConstant; +export function readableStreamToBlob(stream: ReadableStream): Promise<Blob> { + return Promise.resolve(Bun.readableStreamToArray(stream)).$then(array => new Blob(array)); +} + +$linkTimeConstant; +export function consumeReadableStream(nativePtr, nativeType, inputStream) { + const symbol = globalThis.Symbol.for("Bun.consumeReadableStreamPrototype"); + var cached = globalThis[symbol]; + if (!cached) { + cached = globalThis[symbol] = []; + } + var Prototype = cached[nativeType]; + if (Prototype === undefined) { + var [doRead, doError, doReadMany, doClose, onClose, deinit] = + globalThis[globalThis.Symbol.for("Bun.lazy")](nativeType); + + Prototype = class NativeReadableStreamSink { + handleError: any; + handleClosed: any; + processResult: any; + + constructor(reader, ptr) { + this.#ptr = ptr; + this.#reader = reader; + this.#didClose = false; + + this.handleError = this._handleError.bind(this); + this.handleClosed = this._handleClosed.bind(this); + this.processResult = this._processResult.bind(this); + + reader.closed.then(this.handleClosed, this.handleError); + } + + _handleClosed() { + if (this.#didClose) return; + this.#didClose = true; + var ptr = this.#ptr; + this.#ptr = 0; + doClose(ptr); + deinit(ptr); + } + + _handleError(error) { + if (this.#didClose) return; + this.#didClose = true; + var ptr = this.#ptr; + this.#ptr = 0; + doError(ptr, error); + deinit(ptr); + } + + #ptr; + #didClose = false; + #reader; + + _handleReadMany({ value, done, size }) { + if (done) { + this.handleClosed(); + return; + } + + if (this.#didClose) return; + + doReadMany(this.#ptr, value, done, size); + } + + read() { + if (!this.#ptr) return $throwTypeError("ReadableStreamSink is already closed"); + + return this.processResult(this.#reader.read()); + } + + _processResult(result) { + if (result && $isPromise(result)) { + const flags = $getPromiseInternalField(result, $promiseFieldFlags); + if (flags & $promiseStateFulfilled) { + const fulfilledValue = $getPromiseInternalField(result, $promiseFieldReactionsOrResult); + if (fulfilledValue) { + result = fulfilledValue; + } + } + } + + if (result && $isPromise(result)) { + result.then(this.processResult, this.handleError); + return null; + } + + if (result.done) { + this.handleClosed(); + return 0; + } else if (result.value) { + return result.value; + } else { + return -1; + } + } + + readMany() { + if (!this.#ptr) return $throwTypeError("ReadableStreamSink is already closed"); + return this.processResult(this.#reader.readMany()); + } + }; + + const minlength = nativeType + 1; + if (cached.length < minlength) { + cached.length = minlength; + } + $putByValDirect(cached, nativeType, Prototype); + } + + if ($isReadableStreamLocked(inputStream)) { + throw new TypeError("Cannot start reading from a locked stream"); + } + + return new Prototype(inputStream.getReader(), nativePtr); +} + +$linkTimeConstant; +export function createEmptyReadableStream() { + var stream = new ReadableStream({ + pull() {}, + } as any); + $readableStreamClose(stream); + return stream; +} + +$linkTimeConstant; +export function createNativeReadableStream(nativePtr, nativeType, autoAllocateChunkSize) { + return new ReadableStream({ + $lazy: true, + $bunNativeType: nativeType, + $bunNativePtr: nativePtr, + autoAllocateChunkSize: autoAllocateChunkSize, + }); +} + +export function cancel(this, reason) { + if (!$isReadableStream(this)) return Promise.$reject($makeThisTypeError("ReadableStream", "cancel")); + + if ($isReadableStreamLocked(this)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + + return $readableStreamCancel(this, reason); +} + +export function getReader(this, options) { + if (!$isReadableStream(this)) throw $makeThisTypeError("ReadableStream", "getReader"); + + const mode = $toDictionary(options, {}, "ReadableStream.getReader takes an object as first argument").mode; + if (mode === undefined) { + var start_ = $getByIdDirectPrivate(this, "start"); + if (start_) { + $putByIdDirectPrivate(this, "start", undefined); + start_(); + } + + return new ReadableStreamDefaultReader(this); + } + // String conversion is required by spec, hence double equals. + if (mode == "byob") { + return new ReadableStreamBYOBReader(this); + } + + throw new TypeError("Invalid mode is specified"); +} + +export function pipeThrough(this, streams, options) { + const transforms = streams; + + const readable = transforms["readable"]; + if (!$isReadableStream(readable)) throw $makeTypeError("readable should be ReadableStream"); + + const writable = transforms["writable"]; + const internalWritable = $getInternalWritableStream(writable); + if (!$isWritableStream(internalWritable)) throw $makeTypeError("writable should be WritableStream"); + + let preventClose = false; + let preventAbort = false; + let preventCancel = false; + let signal; + if (!$isUndefinedOrNull(options)) { + if (!$isObject(options)) throw $makeTypeError("options must be an object"); + + preventAbort = !!options["preventAbort"]; + preventCancel = !!options["preventCancel"]; + preventClose = !!options["preventClose"]; + + signal = options["signal"]; + if (signal !== undefined && !$isAbortSignal(signal)) throw $makeTypeError("options.signal must be AbortSignal"); + } + + if (!$isReadableStream(this)) throw $makeThisTypeError("ReadableStream", "pipeThrough"); + + if ($isReadableStreamLocked(this)) throw $makeTypeError("ReadableStream is locked"); + + if ($isWritableStreamLocked(internalWritable)) throw $makeTypeError("WritableStream is locked"); + + $readableStreamPipeToWritableStream(this, internalWritable, preventClose, preventAbort, preventCancel, signal); + + return readable; +} + +export function pipeTo(this, destination) { + if (!$isReadableStream(this)) return Promise.$reject($makeThisTypeError("ReadableStream", "pipeTo")); + + if ($isReadableStreamLocked(this)) return Promise.$reject($makeTypeError("ReadableStream is locked")); + + // FIXME: https://bugs.webkit.org/show_bug.cgi?id=159869. + // Built-in generator should be able to parse function signature to compute the function length correctly. + let options = $argument(1); + + let preventClose = false; + let preventAbort = false; + let preventCancel = false; + let signal; + if (!$isUndefinedOrNull(options)) { + if (!$isObject(options)) return Promise.$reject($makeTypeError("options must be an object")); + + try { + preventAbort = !!options["preventAbort"]; + preventCancel = !!options["preventCancel"]; + preventClose = !!options["preventClose"]; + + signal = options["signal"]; + } catch (e) { + return Promise.$reject(e); + } + + if (signal !== undefined && !$isAbortSignal(signal)) + return Promise.$reject(new TypeError("options.signal must be AbortSignal")); + } + + const internalDestination = $getInternalWritableStream(destination); + if (!$isWritableStream(internalDestination)) + return Promise.$reject(new TypeError("ReadableStream pipeTo requires a WritableStream")); + + if ($isWritableStreamLocked(internalDestination)) return Promise.$reject(new TypeError("WritableStream is locked")); + + return $readableStreamPipeToWritableStream( + this, + internalDestination, + preventClose, + preventAbort, + preventCancel, + signal, + ); +} + +export function tee(this) { + if (!$isReadableStream(this)) throw $makeThisTypeError("ReadableStream", "tee"); + + return $readableStreamTee(this, false); +} + +$getter; +export function locked(this) { + if (!$isReadableStream(this)) throw $makeGetterTypeError("ReadableStream", "locked"); + + return $isReadableStreamLocked(this); +} + +export function values(this, options) { + var prototype = ReadableStream.prototype; + $readableStreamDefineLazyIterators(prototype); + return prototype.values.$call(this, options); +} + +$linkTimeConstant; +export function lazyAsyncIterator(this) { + var prototype = ReadableStream.prototype; + $readableStreamDefineLazyIterators(prototype); + return prototype[globalThis.Symbol.asyncIterator].$call(this); +} diff --git a/src/js/builtins/ReadableStreamBYOBReader.ts b/src/js/builtins/ReadableStreamBYOBReader.ts new file mode 100644 index 000000000..5ebfddb19 --- /dev/null +++ b/src/js/builtins/ReadableStreamBYOBReader.ts @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2017 Canon Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeReadableStreamBYOBReader(this, stream) { + if (!$isReadableStream(stream)) throw new TypeError("ReadableStreamBYOBReader needs a ReadableStream"); + if (!$isReadableByteStreamController($getByIdDirectPrivate(stream, "readableStreamController"))) + throw new TypeError("ReadableStreamBYOBReader needs a ReadableByteStreamController"); + if ($isReadableStreamLocked(stream)) throw new TypeError("ReadableStream is locked"); + + $readableStreamReaderGenericInitialize(this, stream); + $putByIdDirectPrivate(this, "readIntoRequests", $createFIFO()); + + return this; +} + +export function cancel(this, reason) { + if (!$isReadableStreamBYOBReader(this)) + return Promise.$reject($makeThisTypeError("ReadableStreamBYOBReader", "cancel")); + + if (!$getByIdDirectPrivate(this, "ownerReadableStream")) + return Promise.$reject($makeTypeError("cancel() called on a reader owned by no readable stream")); + + return $readableStreamReaderGenericCancel(this, reason); +} + +export function read(this, view: DataView) { + if (!$isReadableStreamBYOBReader(this)) + return Promise.$reject($makeThisTypeError("ReadableStreamBYOBReader", "read")); + + if (!$getByIdDirectPrivate(this, "ownerReadableStream")) + return Promise.$reject($makeTypeError("read() called on a reader owned by no readable stream")); + + if (!$isObject(view)) return Promise.$reject($makeTypeError("Provided view is not an object")); + + if (!ArrayBuffer.$isView(view)) return Promise.$reject($makeTypeError("Provided view is not an ArrayBufferView")); + + if (view.byteLength === 0) return Promise.$reject($makeTypeError("Provided view cannot have a 0 byteLength")); + + return $readableStreamBYOBReaderRead(this, view); +} + +export function releaseLock(this) { + if (!$isReadableStreamBYOBReader(this)) throw $makeThisTypeError("ReadableStreamBYOBReader", "releaseLock"); + + if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return; + + if ($getByIdDirectPrivate(this, "readIntoRequests")?.isNotEmpty()) + throw new TypeError("There are still pending read requests, cannot release the lock"); + + $readableStreamReaderGenericRelease(this); +} + +$getter; +export function closed(this) { + if (!$isReadableStreamBYOBReader(this)) + return Promise.$reject($makeGetterTypeError("ReadableStreamBYOBReader", "closed")); + + return $getByIdDirectPrivate(this, "closedPromiseCapability").$promise; +} diff --git a/src/js/builtins/ReadableStreamBYOBRequest.ts b/src/js/builtins/ReadableStreamBYOBRequest.ts new file mode 100644 index 000000000..1354f9349 --- /dev/null +++ b/src/js/builtins/ReadableStreamBYOBRequest.ts @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 Canon Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeReadableStreamBYOBRequest(this, controller, view) { + if (arguments.length !== 3 && arguments[2] !== $isReadableStream) + throw new TypeError("ReadableStreamBYOBRequest constructor should not be called directly"); + + return $privateInitializeReadableStreamBYOBRequest.$call(this, controller, view); +} + +export function respond(this, bytesWritten) { + if (!$isReadableStreamBYOBRequest(this)) throw $makeThisTypeError("ReadableStreamBYOBRequest", "respond"); + + if ($getByIdDirectPrivate(this, "associatedReadableByteStreamController") === undefined) + throw new TypeError("ReadableStreamBYOBRequest.associatedReadableByteStreamController is undefined"); + + return $readableByteStreamControllerRespond( + $getByIdDirectPrivate(this, "associatedReadableByteStreamController"), + bytesWritten, + ); +} + +export function respondWithNewView(this, view) { + if (!$isReadableStreamBYOBRequest(this)) throw $makeThisTypeError("ReadableStreamBYOBRequest", "respond"); + + if ($getByIdDirectPrivate(this, "associatedReadableByteStreamController") === undefined) + throw new TypeError("ReadableStreamBYOBRequest.associatedReadableByteStreamController is undefined"); + + if (!$isObject(view)) throw new TypeError("Provided view is not an object"); + + if (!ArrayBuffer.$isView(view)) throw new TypeError("Provided view is not an ArrayBufferView"); + + return $readableByteStreamControllerRespondWithNewView( + $getByIdDirectPrivate(this, "associatedReadableByteStreamController"), + view, + ); +} + +$getter; +export function view(this) { + if (!$isReadableStreamBYOBRequest(this)) throw $makeGetterTypeError("ReadableStreamBYOBRequest", "view"); + + return $getByIdDirectPrivate(this, "view"); +} diff --git a/src/js/builtins/ReadableStreamDefaultController.ts b/src/js/builtins/ReadableStreamDefaultController.ts new file mode 100644 index 000000000..912cd1acb --- /dev/null +++ b/src/js/builtins/ReadableStreamDefaultController.ts @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 Canon Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeReadableStreamDefaultController(this, stream, underlyingSource, size, highWaterMark) { + if (arguments.length !== 5 && arguments[4] !== $isReadableStream) + throw new TypeError("ReadableStreamDefaultController constructor should not be called directly"); + + return $privateInitializeReadableStreamDefaultController.$call(this, stream, underlyingSource, size, highWaterMark); +} + +export function enqueue(this, chunk) { + if (!$isReadableStreamDefaultController(this)) throw $makeThisTypeError("ReadableStreamDefaultController", "enqueue"); + + if (!$readableStreamDefaultControllerCanCloseOrEnqueue(this)) + throw new TypeError("ReadableStreamDefaultController is not in a state where chunk can be enqueued"); + + return $readableStreamDefaultControllerEnqueue(this, chunk); +} + +export function error(this, err) { + if (!$isReadableStreamDefaultController(this)) throw $makeThisTypeError("ReadableStreamDefaultController", "error"); + + $readableStreamDefaultControllerError(this, err); +} + +export function close(this) { + if (!$isReadableStreamDefaultController(this)) throw $makeThisTypeError("ReadableStreamDefaultController", "close"); + + if (!$readableStreamDefaultControllerCanCloseOrEnqueue(this)) + throw new TypeError("ReadableStreamDefaultController is not in a state where it can be closed"); + + $readableStreamDefaultControllerClose(this); +} + +$getter; +export function desiredSize(this) { + if (!$isReadableStreamDefaultController(this)) + throw $makeGetterTypeError("ReadableStreamDefaultController", "desiredSize"); + + return $readableStreamDefaultControllerGetDesiredSize(this); +} diff --git a/src/js/builtins/ReadableStreamDefaultReader.ts b/src/js/builtins/ReadableStreamDefaultReader.ts new file mode 100644 index 000000000..ecd553ed5 --- /dev/null +++ b/src/js/builtins/ReadableStreamDefaultReader.ts @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2015 Canon Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeReadableStreamDefaultReader(this, stream) { + if (!$isReadableStream(stream)) throw new TypeError("ReadableStreamDefaultReader needs a ReadableStream"); + if ($isReadableStreamLocked(stream)) throw new TypeError("ReadableStream is locked"); + + $readableStreamReaderGenericInitialize(this, stream); + $putByIdDirectPrivate(this, "readRequests", $createFIFO()); + + return this; +} + +export function cancel(this, reason) { + if (!$isReadableStreamDefaultReader(this)) + return Promise.$reject($makeThisTypeError("ReadableStreamDefaultReader", "cancel")); + + if (!$getByIdDirectPrivate(this, "ownerReadableStream")) + return Promise.$reject(new TypeError("cancel() called on a reader owned by no readable stream")); + + return $readableStreamReaderGenericCancel(this, reason); +} + +export function readMany(this) { + if (!$isReadableStreamDefaultReader(this)) + throw new TypeError("ReadableStreamDefaultReader.readMany() should not be called directly"); + + const stream = $getByIdDirectPrivate(this, "ownerReadableStream"); + if (!stream) throw new TypeError("readMany() called on a reader owned by no readable stream"); + + const state = $getByIdDirectPrivate(stream, "state"); + $putByIdDirectPrivate(stream, "disturbed", true); + if (state === $streamClosed) return { value: [], size: 0, done: true }; + else if (state === $streamErrored) { + throw $getByIdDirectPrivate(stream, "storedError"); + } + + var controller = $getByIdDirectPrivate(stream, "readableStreamController"); + var queue = $getByIdDirectPrivate(controller, "queue"); + + if (!queue) { + // This is a ReadableStream direct controller implemented in JS + // It hasn't been started yet. + return controller.$pull(controller).$then(function ({ done, value }) { + return done ? { done: true, value: [], size: 0 } : { value: [value], size: 1, done: false }; + }); + } + + const content = queue.content; + var size = queue.size; + var values = content.toArray(false); + + var length = values.length; + + if (length > 0) { + var outValues = $newArrayWithSize(length); + if ($isReadableByteStreamController(controller)) { + { + const buf = values[0]; + if (!(ArrayBuffer.$isView(buf) || buf instanceof ArrayBuffer)) { + $putByValDirect(outValues, 0, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)); + } else { + $putByValDirect(outValues, 0, buf); + } + } + + for (var i = 1; i < length; i++) { + const buf = values[i]; + if (!(ArrayBuffer.$isView(buf) || buf instanceof ArrayBuffer)) { + $putByValDirect(outValues, i, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)); + } else { + $putByValDirect(outValues, i, buf); + } + } + } else { + $putByValDirect(outValues, 0, values[0].value); + for (var i = 1; i < length; i++) { + $putByValDirect(outValues, i, values[i].value); + } + } + + $resetQueue($getByIdDirectPrivate(controller, "queue")); + + if ($getByIdDirectPrivate(controller, "closeRequested")) + $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); + else if ($isReadableStreamDefaultController(controller)) + $readableStreamDefaultControllerCallPullIfNeeded(controller); + else if ($isReadableByteStreamController(controller)) $readableByteStreamControllerCallPullIfNeeded(controller); + + return { value: outValues, size, done: false }; + } + + var onPullMany = result => { + if (result.done) { + return { value: [], size: 0, done: true }; + } + var controller = $getByIdDirectPrivate(stream, "readableStreamController"); + + var queue = $getByIdDirectPrivate(controller, "queue"); + var value = [result.value].concat(queue.content.toArray(false)); + var length = value.length; + + if ($isReadableByteStreamController(controller)) { + for (var i = 0; i < length; i++) { + const buf = value[i]; + if (!(ArrayBuffer.$isView(buf) || buf instanceof ArrayBuffer)) { + const { buffer, byteOffset, byteLength } = buf; + $putByValDirect(value, i, new Uint8Array(buffer, byteOffset, byteLength)); + } + } + } else { + for (var i = 1; i < length; i++) { + $putByValDirect(value, i, value[i].value); + } + } + + var size = queue.size; + $resetQueue(queue); + + if ($getByIdDirectPrivate(controller, "closeRequested")) + $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); + else if ($isReadableStreamDefaultController(controller)) + $readableStreamDefaultControllerCallPullIfNeeded(controller); + else if ($isReadableByteStreamController(controller)) $readableByteStreamControllerCallPullIfNeeded(controller); + + return { value: value, size: size, done: false }; + }; + + var pullResult = controller.$pull(controller); + if (pullResult && $isPromise(pullResult)) { + return pullResult.$then(onPullMany); + } + + return onPullMany(pullResult); +} + +export function read(this) { + if (!$isReadableStreamDefaultReader(this)) + return Promise.$reject($makeThisTypeError("ReadableStreamDefaultReader", "read")); + if (!$getByIdDirectPrivate(this, "ownerReadableStream")) + return Promise.$reject(new TypeError("read() called on a reader owned by no readable stream")); + + return $readableStreamDefaultReaderRead(this); +} + +export function releaseLock(this) { + if (!$isReadableStreamDefaultReader(this)) throw $makeThisTypeError("ReadableStreamDefaultReader", "releaseLock"); + + if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return; + + if ($getByIdDirectPrivate(this, "readRequests")?.isNotEmpty()) + throw new TypeError("There are still pending read requests, cannot release the lock"); + + $readableStreamReaderGenericRelease(this); +} + +$getter; +export function closed(this) { + if (!$isReadableStreamDefaultReader(this)) + return Promise.$reject($makeGetterTypeError("ReadableStreamDefaultReader", "closed")); + + return $getByIdDirectPrivate(this, "closedPromiseCapability").$promise; +} diff --git a/src/js/builtins/ReadableStreamInternals.ts b/src/js/builtins/ReadableStreamInternals.ts new file mode 100644 index 000000000..0c4e816f4 --- /dev/null +++ b/src/js/builtins/ReadableStreamInternals.ts @@ -0,0 +1,1799 @@ +/* + * Copyright (C) 2015 Canon Inc. All rights reserved. + * Copyright (C) 2015 Igalia. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// @internal + +export function readableStreamReaderGenericInitialize(reader, stream) { + $putByIdDirectPrivate(reader, "ownerReadableStream", stream); + $putByIdDirectPrivate(stream, "reader", reader); + if ($getByIdDirectPrivate(stream, "state") === $streamReadable) + $putByIdDirectPrivate(reader, "closedPromiseCapability", $newPromiseCapability(Promise)); + else if ($getByIdDirectPrivate(stream, "state") === $streamClosed) + $putByIdDirectPrivate(reader, "closedPromiseCapability", { + $promise: Promise.$resolve(), + }); + else { + $assert($getByIdDirectPrivate(stream, "state") === $streamErrored); + $putByIdDirectPrivate(reader, "closedPromiseCapability", { + $promise: $newHandledRejectedPromise($getByIdDirectPrivate(stream, "storedError")), + }); + } +} + +export function privateInitializeReadableStreamDefaultController(this, stream, underlyingSource, size, highWaterMark) { + if (!$isReadableStream(stream)) throw new TypeError("ReadableStreamDefaultController needs a ReadableStream"); + + // readableStreamController is initialized with null value. + if ($getByIdDirectPrivate(stream, "readableStreamController") !== null) + throw new TypeError("ReadableStream already has a controller"); + + $putByIdDirectPrivate(this, "controlledReadableStream", stream); + $putByIdDirectPrivate(this, "underlyingSource", underlyingSource); + $putByIdDirectPrivate(this, "queue", $newQueue()); + $putByIdDirectPrivate(this, "started", -1); + $putByIdDirectPrivate(this, "closeRequested", false); + $putByIdDirectPrivate(this, "pullAgain", false); + $putByIdDirectPrivate(this, "pulling", false); + $putByIdDirectPrivate(this, "strategy", $validateAndNormalizeQueuingStrategy(size, highWaterMark)); + + return this; +} + +export function readableStreamDefaultControllerError(controller, error) { + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + if ($getByIdDirectPrivate(stream, "state") !== $streamReadable) return; + $putByIdDirectPrivate(controller, "queue", $newQueue()); + + $readableStreamError(stream, error); +} + +export function readableStreamPipeTo(stream, sink) { + $assert($isReadableStream(stream)); + + const reader = new ReadableStreamDefaultReader(stream); + + $getByIdDirectPrivate(reader, "closedPromiseCapability").$promise.$then( + () => {}, + e => { + sink.error(e); + }, + ); + + function doPipe() { + $readableStreamDefaultReaderRead(reader).$then( + function (result) { + if (result.done) { + sink.close(); + return; + } + try { + sink.enqueue(result.value); + } catch (e) { + sink.error("ReadableStream chunk enqueueing in the sink failed"); + return; + } + doPipe(); + }, + function (e) { + sink.error(e); + }, + ); + } + doPipe(); +} + +export function acquireReadableStreamDefaultReader(stream) { + var start = $getByIdDirectPrivate(stream, "start"); + if (start) { + start.$call(stream); + } + + return new ReadableStreamDefaultReader(stream); +} + +// https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller, starting from step 6. +// The other part is implemented in privateInitializeReadableStreamDefaultController. +export function setupReadableStreamDefaultController( + stream, + underlyingSource, + size, + highWaterMark, + startMethod, + pullMethod, + cancelMethod, +) { + const controller = new ReadableStreamDefaultController( + stream, + underlyingSource, + size, + highWaterMark, + $isReadableStream, + ); + + const pullAlgorithm = () => $promiseInvokeOrNoopMethod(underlyingSource, pullMethod, [controller]); + const cancelAlgorithm = reason => $promiseInvokeOrNoopMethod(underlyingSource, cancelMethod, [reason]); + + $putByIdDirectPrivate(controller, "pullAlgorithm", pullAlgorithm); + $putByIdDirectPrivate(controller, "cancelAlgorithm", cancelAlgorithm); + $putByIdDirectPrivate(controller, "pull", $readableStreamDefaultControllerPull); + $putByIdDirectPrivate(controller, "cancel", $readableStreamDefaultControllerCancel); + $putByIdDirectPrivate(stream, "readableStreamController", controller); + + $readableStreamDefaultControllerStart(controller); +} + +export function createReadableStreamController(stream, underlyingSource, strategy) { + const type = underlyingSource.type; + const typeString = $toString(type); + + if (typeString === "bytes") { + // if (!$readableByteStreamAPIEnabled()) + // $throwTypeError("ReadableByteStreamController is not implemented"); + + if (strategy.highWaterMark === undefined) strategy.highWaterMark = 0; + if (strategy.size !== undefined) $throwRangeError("Strategy for a ReadableByteStreamController cannot have a size"); + + $putByIdDirectPrivate( + stream, + "readableStreamController", + new ReadableByteStreamController(stream, underlyingSource, strategy.highWaterMark, $isReadableStream), + ); + } else if (typeString === "direct") { + var highWaterMark = strategy?.highWaterMark; + $initializeArrayBufferStream.$call(stream, underlyingSource, highWaterMark); + } else if (type === undefined) { + if (strategy.highWaterMark === undefined) strategy.highWaterMark = 1; + + $setupReadableStreamDefaultController( + stream, + underlyingSource, + strategy.size, + strategy.highWaterMark, + underlyingSource.start, + underlyingSource.pull, + underlyingSource.cancel, + ); + } else throw new RangeError("Invalid type for underlying source"); +} + +export function readableStreamDefaultControllerStart(controller) { + if ($getByIdDirectPrivate(controller, "started") !== -1) return; + + const underlyingSource = $getByIdDirectPrivate(controller, "underlyingSource"); + const startMethod = underlyingSource.start; + $putByIdDirectPrivate(controller, "started", 0); + + $promiseInvokeOrNoopMethodNoCatch(underlyingSource, startMethod, [controller]).$then( + () => { + $putByIdDirectPrivate(controller, "started", 1); + $assert(!$getByIdDirectPrivate(controller, "pulling")); + $assert(!$getByIdDirectPrivate(controller, "pullAgain")); + $readableStreamDefaultControllerCallPullIfNeeded(controller); + }, + error => { + $readableStreamDefaultControllerError(controller, error); + }, + ); +} + +// FIXME: Replace readableStreamPipeTo by below function. +// This method implements the latest https://streams.spec.whatwg.org/#readable-stream-pipe-to. +export function readableStreamPipeToWritableStream( + source, + destination, + preventClose, + preventAbort, + preventCancel, + signal, +) { + // const isDirectStream = !!$getByIdDirectPrivate(source, "start"); + + $assert($isReadableStream(source)); + $assert($isWritableStream(destination)); + $assert(!$isReadableStreamLocked(source)); + $assert(!$isWritableStreamLocked(destination)); + $assert(signal === undefined || $isAbortSignal(signal)); + + if ($getByIdDirectPrivate(source, "underlyingByteSource") !== undefined) + return Promise.$reject("Piping to a readable bytestream is not supported"); + + let pipeState: any = { + source: source, + destination: destination, + preventAbort: preventAbort, + preventCancel: preventCancel, + preventClose: preventClose, + signal: signal, + }; + + pipeState.reader = $acquireReadableStreamDefaultReader(source); + pipeState.writer = $acquireWritableStreamDefaultWriter(destination); + + $putByIdDirectPrivate(source, "disturbed", true); + + pipeState.finalized = false; + pipeState.shuttingDown = false; + pipeState.promiseCapability = $newPromiseCapability(Promise); + pipeState.pendingReadPromiseCapability = $newPromiseCapability(Promise); + pipeState.pendingReadPromiseCapability.$resolve.$call(); + pipeState.pendingWritePromise = Promise.$resolve(); + + if (signal !== undefined) { + const algorithm = reason => { + if (pipeState.finalized) return; + + $pipeToShutdownWithAction( + pipeState, + () => { + const shouldAbortDestination = + !pipeState.preventAbort && $getByIdDirectPrivate(pipeState.destination, "state") === "writable"; + const promiseDestination = shouldAbortDestination + ? $writableStreamAbort(pipeState.destination, reason) + : Promise.$resolve(); + + const shouldAbortSource = + !pipeState.preventCancel && $getByIdDirectPrivate(pipeState.source, "state") === $streamReadable; + const promiseSource = shouldAbortSource + ? $readableStreamCancel(pipeState.source, reason) + : Promise.$resolve(); + + let promiseCapability = $newPromiseCapability(Promise); + let shouldWait = true; + let handleResolvedPromise = () => { + if (shouldWait) { + shouldWait = false; + return; + } + promiseCapability.$resolve.$call(); + }; + let handleRejectedPromise = e => { + promiseCapability.$reject.$call(undefined, e); + }; + promiseDestination.$then(handleResolvedPromise, handleRejectedPromise); + promiseSource.$then(handleResolvedPromise, handleRejectedPromise); + return promiseCapability.$promise; + }, + reason, + ); + }; + if ($whenSignalAborted(signal, algorithm)) return pipeState.promiseCapability.$promise; + } + + $pipeToErrorsMustBePropagatedForward(pipeState); + $pipeToErrorsMustBePropagatedBackward(pipeState); + $pipeToClosingMustBePropagatedForward(pipeState); + $pipeToClosingMustBePropagatedBackward(pipeState); + + $pipeToLoop(pipeState); + + return pipeState.promiseCapability.$promise; +} + +export function pipeToLoop(pipeState) { + if (pipeState.shuttingDown) return; + + $pipeToDoReadWrite(pipeState).$then(result => { + if (result) $pipeToLoop(pipeState); + }); +} + +export function pipeToDoReadWrite(pipeState) { + $assert(!pipeState.shuttingDown); + + pipeState.pendingReadPromiseCapability = $newPromiseCapability(Promise); + $getByIdDirectPrivate(pipeState.writer, "readyPromise").$promise.$then( + () => { + if (pipeState.shuttingDown) { + pipeState.pendingReadPromiseCapability.$resolve.$call(undefined, false); + return; + } + + $readableStreamDefaultReaderRead(pipeState.reader).$then( + result => { + const canWrite = !result.done && $getByIdDirectPrivate(pipeState.writer, "stream") !== undefined; + pipeState.pendingReadPromiseCapability.$resolve.$call(undefined, canWrite); + if (!canWrite) return; + + pipeState.pendingWritePromise = $writableStreamDefaultWriterWrite(pipeState.writer, result.value); + }, + e => { + pipeState.pendingReadPromiseCapability.$resolve.$call(undefined, false); + }, + ); + }, + e => { + pipeState.pendingReadPromiseCapability.$resolve.$call(undefined, false); + }, + ); + return pipeState.pendingReadPromiseCapability.$promise; +} + +export function pipeToErrorsMustBePropagatedForward(pipeState) { + const action = () => { + pipeState.pendingReadPromiseCapability.$resolve.$call(undefined, false); + const error = $getByIdDirectPrivate(pipeState.source, "storedError"); + if (!pipeState.preventAbort) { + $pipeToShutdownWithAction(pipeState, () => $writableStreamAbort(pipeState.destination, error), error); + return; + } + $pipeToShutdown(pipeState, error); + }; + + if ($getByIdDirectPrivate(pipeState.source, "state") === $streamErrored) { + action(); + return; + } + + $getByIdDirectPrivate(pipeState.reader, "closedPromiseCapability").$promise.$then(undefined, action); +} + +export function pipeToErrorsMustBePropagatedBackward(pipeState) { + const action = () => { + const error = $getByIdDirectPrivate(pipeState.destination, "storedError"); + if (!pipeState.preventCancel) { + $pipeToShutdownWithAction(pipeState, () => $readableStreamCancel(pipeState.source, error), error); + return; + } + $pipeToShutdown(pipeState, error); + }; + if ($getByIdDirectPrivate(pipeState.destination, "state") === "errored") { + action(); + return; + } + $getByIdDirectPrivate(pipeState.writer, "closedPromise").$promise.$then(undefined, action); +} + +export function pipeToClosingMustBePropagatedForward(pipeState) { + const action = () => { + pipeState.pendingReadPromiseCapability.$resolve.$call(undefined, false); + // const error = $getByIdDirectPrivate(pipeState.source, "storedError"); + if (!pipeState.preventClose) { + $pipeToShutdownWithAction(pipeState, () => + $writableStreamDefaultWriterCloseWithErrorPropagation(pipeState.writer), + ); + return; + } + $pipeToShutdown(pipeState); + }; + if ($getByIdDirectPrivate(pipeState.source, "state") === $streamClosed) { + action(); + return; + } + $getByIdDirectPrivate(pipeState.reader, "closedPromiseCapability").$promise.$then(action, undefined); +} + +export function pipeToClosingMustBePropagatedBackward(pipeState) { + if ( + !$writableStreamCloseQueuedOrInFlight(pipeState.destination) && + $getByIdDirectPrivate(pipeState.destination, "state") !== "closed" + ) + return; + + // $assert no chunks have been read/written + + const error = new TypeError("closing is propagated backward"); + if (!pipeState.preventCancel) { + $pipeToShutdownWithAction(pipeState, () => $readableStreamCancel(pipeState.source, error), error); + return; + } + $pipeToShutdown(pipeState, error); +} + +export function pipeToShutdownWithAction(pipeState, action) { + if (pipeState.shuttingDown) return; + + pipeState.shuttingDown = true; + + const hasError = arguments.length > 2; + const error = arguments[2]; + const finalize = () => { + const promise = action(); + promise.$then( + () => { + if (hasError) $pipeToFinalize(pipeState, error); + else $pipeToFinalize(pipeState); + }, + e => { + $pipeToFinalize(pipeState, e); + }, + ); + }; + + if ( + $getByIdDirectPrivate(pipeState.destination, "state") === "writable" && + !$writableStreamCloseQueuedOrInFlight(pipeState.destination) + ) { + pipeState.pendingReadPromiseCapability.$promise.$then( + () => { + pipeState.pendingWritePromise.$then(finalize, finalize); + }, + e => $pipeToFinalize(pipeState, e), + ); + return; + } + + finalize(); +} + +export function pipeToShutdown(pipeState) { + if (pipeState.shuttingDown) return; + + pipeState.shuttingDown = true; + + const hasError = arguments.length > 1; + const error = arguments[1]; + const finalize = () => { + if (hasError) $pipeToFinalize(pipeState, error); + else $pipeToFinalize(pipeState); + }; + + if ( + $getByIdDirectPrivate(pipeState.destination, "state") === "writable" && + !$writableStreamCloseQueuedOrInFlight(pipeState.destination) + ) { + pipeState.pendingReadPromiseCapability.$promise.$then( + () => { + pipeState.pendingWritePromise.$then(finalize, finalize); + }, + e => $pipeToFinalize(pipeState, e), + ); + return; + } + finalize(); +} + +export function pipeToFinalize(pipeState) { + $writableStreamDefaultWriterRelease(pipeState.writer); + $readableStreamReaderGenericRelease(pipeState.reader); + + // Instead of removing the abort algorithm as per spec, we make it a no-op which is equivalent. + pipeState.finalized = true; + + if (arguments.length > 1) pipeState.promiseCapability.$reject.$call(undefined, arguments[1]); + else pipeState.promiseCapability.$resolve.$call(); +} + +export function readableStreamTee(stream, shouldClone) { + $assert($isReadableStream(stream)); + $assert(typeof shouldClone === "boolean"); + + var start_ = $getByIdDirectPrivate(stream, "start"); + if (start_) { + $putByIdDirectPrivate(stream, "start", undefined); + start_(); + } + + const reader = new $ReadableStreamDefaultReader(stream); + + const teeState = { + closedOrErrored: false, + canceled1: false, + canceled2: false, + reason1: undefined, + reason2: undefined, + }; + + teeState.cancelPromiseCapability = $newPromiseCapability(Promise); + + const pullFunction = $readableStreamTeePullFunction(teeState, reader, shouldClone); + + const branch1Source = {}; + $putByIdDirectPrivate(branch1Source, "pull", pullFunction); + $putByIdDirectPrivate(branch1Source, "cancel", $readableStreamTeeBranch1CancelFunction(teeState, stream)); + + const branch2Source = {}; + $putByIdDirectPrivate(branch2Source, "pull", pullFunction); + $putByIdDirectPrivate(branch2Source, "cancel", $readableStreamTeeBranch2CancelFunction(teeState, stream)); + + const branch1 = new $ReadableStream(branch1Source); + const branch2 = new $ReadableStream(branch2Source); + + $getByIdDirectPrivate(reader, "closedPromiseCapability").$promise.$then(undefined, function (e) { + if (teeState.closedOrErrored) return; + $readableStreamDefaultControllerError(branch1.$readableStreamController, e); + $readableStreamDefaultControllerError(branch2.$readableStreamController, e); + teeState.closedOrErrored = true; + if (!teeState.canceled1 || !teeState.canceled2) teeState.cancelPromiseCapability.$resolve.$call(); + }); + + // Additional fields compared to the spec, as they are needed within pull/cancel functions. + teeState.branch1 = branch1; + teeState.branch2 = branch2; + + return [branch1, branch2]; +} + +export function readableStreamTeePullFunction(teeState, reader, shouldClone) { + return function () { + Promise.prototype.$then.$call($readableStreamDefaultReaderRead(reader), function (result) { + $assert($isObject(result)); + $assert(typeof result.done === "boolean"); + if (result.done && !teeState.closedOrErrored) { + if (!teeState.canceled1) $readableStreamDefaultControllerClose(teeState.branch1.$readableStreamController); + if (!teeState.canceled2) $readableStreamDefaultControllerClose(teeState.branch2.$readableStreamController); + teeState.closedOrErrored = true; + if (!teeState.canceled1 || !teeState.canceled2) teeState.cancelPromiseCapability.$resolve.$call(); + } + if (teeState.closedOrErrored) return; + if (!teeState.canceled1) + $readableStreamDefaultControllerEnqueue(teeState.branch1.$readableStreamController, result.value); + if (!teeState.canceled2) + $readableStreamDefaultControllerEnqueue( + teeState.branch2.$readableStreamController, + shouldClone ? $structuredCloneForStream(result.value) : result.value, + ); + }); + }; +} + +export function readableStreamTeeBranch1CancelFunction(teeState, stream) { + return function (r) { + teeState.canceled1 = true; + teeState.reason1 = r; + if (teeState.canceled2) { + $readableStreamCancel(stream, [teeState.reason1, teeState.reason2]).$then( + teeState.cancelPromiseCapability.$resolve, + teeState.cancelPromiseCapability.$reject, + ); + } + return teeState.cancelPromiseCapability.$promise; + }; +} + +export function readableStreamTeeBranch2CancelFunction(teeState, stream) { + return function (r) { + teeState.canceled2 = true; + teeState.reason2 = r; + if (teeState.canceled1) { + $readableStreamCancel(stream, [teeState.reason1, teeState.reason2]).$then( + teeState.cancelPromiseCapability.$resolve, + teeState.cancelPromiseCapability.$reject, + ); + } + return teeState.cancelPromiseCapability.$promise; + }; +} + +export function isReadableStream(stream) { + // Spec tells to return true only if stream has a readableStreamController internal slot. + // However, since it is a private slot, it cannot be checked using hasOwnProperty(). + // Therefore, readableStreamController is initialized with null value. + return $isObject(stream) && $getByIdDirectPrivate(stream, "readableStreamController") !== undefined; +} + +export function isReadableStreamDefaultReader(reader) { + // Spec tells to return true only if reader has a readRequests internal slot. + // However, since it is a private slot, it cannot be checked using hasOwnProperty(). + // Since readRequests is initialized with an empty array, the following test is ok. + return $isObject(reader) && !!$getByIdDirectPrivate(reader, "readRequests"); +} + +export function isReadableStreamDefaultController(controller) { + // Spec tells to return true only if controller has an underlyingSource internal slot. + // However, since it is a private slot, it cannot be checked using hasOwnProperty(). + // underlyingSource is obtained in ReadableStream constructor: if undefined, it is set + // to an empty object. Therefore, following test is ok. + return $isObject(controller) && !!$getByIdDirectPrivate(controller, "underlyingSource"); +} + +export function readDirectStream(stream, sink, underlyingSource) { + $putByIdDirectPrivate(stream, "underlyingSource", undefined); + $putByIdDirectPrivate(stream, "start", undefined); + + function close(stream, reason) { + if (reason && underlyingSource?.cancel) { + try { + var prom = underlyingSource.cancel(reason); + $markPromiseAsHandled(prom); + } catch (e) {} + + underlyingSource = undefined; + } + + if (stream) { + $putByIdDirectPrivate(stream, "readableStreamController", undefined); + $putByIdDirectPrivate(stream, "reader", undefined); + if (reason) { + $putByIdDirectPrivate(stream, "state", $streamErrored); + $putByIdDirectPrivate(stream, "storedError", reason); + } else { + $putByIdDirectPrivate(stream, "state", $streamClosed); + } + stream = undefined; + } + } + + if (!underlyingSource.pull) { + close(); + return; + } + + if (!$isCallable(underlyingSource.pull)) { + close(); + $throwTypeError("pull is not a function"); + return; + } + + $putByIdDirectPrivate(stream, "readableStreamController", sink); + const highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark"); + + sink.start({ + highWaterMark: !highWaterMark || highWaterMark < 64 ? 64 : highWaterMark, + }); + + $startDirectStream.$call(sink, stream, underlyingSource.pull, close); + $putByIdDirectPrivate(stream, "reader", {}); + + var maybePromise = underlyingSource.pull(sink); + sink = undefined; + if (maybePromise && $isPromise(maybePromise)) { + return maybePromise.$then(() => {}); + } +} + +$linkTimeConstant; +export function assignToStream(stream, sink) { + // The stream is either a direct stream or a "default" JS stream + var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); + + // we know it's a direct stream when $underlyingSource is set + if (underlyingSource) { + try { + return $readDirectStream(stream, sink, underlyingSource); + } catch (e) { + throw e; + } finally { + underlyingSource = undefined; + stream = undefined; + sink = undefined; + } + } + + return $readStreamIntoSink(stream, sink, true); +} + +export async function readStreamIntoSink(stream, sink, isNative) { + var didClose = false; + var didThrow = false; + try { + var reader = stream.getReader(); + var many = reader.readMany(); + if (many && $isPromise(many)) { + many = await many; + } + if (many.done) { + didClose = true; + return sink.end(); + } + var wroteCount = many.value.length; + const highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark"); + if (isNative) + $startDirectStream.$call(sink, stream, undefined, () => !didThrow && $markPromiseAsHandled(stream.cancel())); + + sink.start({ highWaterMark: highWaterMark || 0 }); + + for (var i = 0, values = many.value, length = many.value.length; i < length; i++) { + sink.write(values[i]); + } + + var streamState = $getByIdDirectPrivate(stream, "state"); + if (streamState === $streamClosed) { + didClose = true; + return sink.end(); + } + + while (true) { + var { value, done } = await reader.read(); + if (done) { + didClose = true; + return sink.end(); + } + + sink.write(value); + } + } catch (e) { + didThrow = true; + + try { + reader = undefined; + const prom = stream.cancel(e); + $markPromiseAsHandled(prom); + } catch (j) {} + + if (sink && !didClose) { + didClose = true; + try { + sink.close(e); + } catch (j) { + throw new globalThis.AggregateError([e, j]); + } + } + + throw e; + } finally { + if (reader) { + try { + reader.releaseLock(); + } catch (e) {} + reader = undefined; + } + sink = undefined; + var streamState = $getByIdDirectPrivate(stream, "state"); + if (stream) { + // make it easy for this to be GC'd + // but don't do property transitions + var readableStreamController = $getByIdDirectPrivate(stream, "readableStreamController"); + if (readableStreamController) { + if ($getByIdDirectPrivate(readableStreamController, "underlyingSource")) + $putByIdDirectPrivate(readableStreamController, "underlyingSource", undefined); + if ($getByIdDirectPrivate(readableStreamController, "controlledReadableStream")) + $putByIdDirectPrivate(readableStreamController, "controlledReadableStream", undefined); + + $putByIdDirectPrivate(stream, "readableStreamController", null); + if ($getByIdDirectPrivate(stream, "underlyingSource")) + $putByIdDirectPrivate(stream, "underlyingSource", undefined); + readableStreamController = undefined; + } + + if (!didThrow && streamState !== $streamClosed && streamState !== $streamErrored) { + $readableStreamClose(stream); + } + stream = undefined; + } + } +} + +export function handleDirectStreamError(e) { + var controller = this; + var sink = controller.$sink; + if (sink) { + $putByIdDirectPrivate(controller, "sink", undefined); + try { + sink.close(e); + } catch (f) {} + } + + this.error = this.flush = this.write = this.close = this.end = $onReadableStreamDirectControllerClosed; + + if (typeof this.$underlyingSource.close === "function") { + try { + this.$underlyingSource.close.$call(this.$underlyingSource, e); + } catch (e) {} + } + + try { + var pend = controller._pendingRead; + if (pend) { + controller._pendingRead = undefined; + $rejectPromise(pend, e); + } + } catch (f) {} + var stream = controller.$controlledReadableStream; + if (stream) $readableStreamError(stream, e); +} + +export function handleDirectStreamErrorReject(e) { + $handleDirectStreamError.$call(this, e); + return Promise.$reject(e); +} + +export function onPullDirectStream(controller) { + var stream = controller.$controlledReadableStream; + if (!stream || $getByIdDirectPrivate(stream, "state") !== $streamReadable) return; + + // pull is in progress + // this is a recursive call + // ignore it + if (controller._deferClose === -1) { + return; + } + + controller._deferClose = -1; + controller._deferFlush = -1; + var deferClose; + var deferFlush; + + // Direct streams allow $pull to be called multiple times, unlike the spec. + // Backpressure is handled by the destination, not by the underlying source. + // In this case, we rely on the heuristic that repeatedly draining in the same tick + // is bad for performance + // this code is only run when consuming a direct stream from JS + // without the HTTP server or anything else + try { + var result = controller.$underlyingSource.pull(controller); + + if (result && $isPromise(result)) { + if (controller._handleError === undefined) { + controller._handleError = $handleDirectStreamErrorReject.bind(controller); + } + + Promise.prototype.catch.$call(result, controller._handleError); + } + } catch (e) { + return $handleDirectStreamErrorReject.$call(controller, e); + } finally { + deferClose = controller._deferClose; + deferFlush = controller._deferFlush; + controller._deferFlush = controller._deferClose = 0; + } + + var promiseToReturn; + + if (controller._pendingRead === undefined) { + controller._pendingRead = promiseToReturn = $newPromise(); + } else { + promiseToReturn = $readableStreamAddReadRequest(stream); + } + + // they called close during $pull() + // we delay that + if (deferClose === 1) { + var reason = controller._deferCloseReason; + controller._deferCloseReason = undefined; + $onCloseDirectStream.$call(controller, reason); + return promiseToReturn; + } + + // not done, but they called flush() + if (deferFlush === 1) { + $onFlushDirectStream.$call(controller); + } + + return promiseToReturn; +} + +export function noopDoneFunction() { + return Promise.$resolve({ value: undefined, done: true }); +} + +export function onReadableStreamDirectControllerClosed(reason) { + $throwTypeError("ReadableStreamDirectController is now closed"); +} + +export function onCloseDirectStream(reason) { + var stream = this.$controlledReadableStream; + if (!stream || $getByIdDirectPrivate(stream, "state") !== $streamReadable) return; + + if (this._deferClose !== 0) { + this._deferClose = 1; + this._deferCloseReason = reason; + return; + } + + $putByIdDirectPrivate(stream, "state", $streamClosing); + if (typeof this.$underlyingSource.close === "function") { + try { + this.$underlyingSource.close.$call(this.$underlyingSource, reason); + } catch (e) {} + } + + var flushed; + try { + flushed = this.$sink.end(); + $putByIdDirectPrivate(this, "sink", undefined); + } catch (e) { + if (this._pendingRead) { + var read = this._pendingRead; + this._pendingRead = undefined; + $rejectPromise(read, e); + } + $readableStreamError(stream, e); + return; + } + + this.error = this.flush = this.write = this.close = this.end = $onReadableStreamDirectControllerClosed; + + var reader = $getByIdDirectPrivate(stream, "reader"); + + if (reader && $isReadableStreamDefaultReader(reader)) { + var _pendingRead = this._pendingRead; + if (_pendingRead && $isPromise(_pendingRead) && flushed?.byteLength) { + this._pendingRead = undefined; + $fulfillPromise(_pendingRead, { value: flushed, done: false }); + $readableStreamClose(stream); + return; + } + } + + if (flushed?.byteLength) { + var requests = $getByIdDirectPrivate(reader, "readRequests"); + if (requests?.isNotEmpty()) { + $readableStreamFulfillReadRequest(stream, flushed, false); + $readableStreamClose(stream); + return; + } + + $putByIdDirectPrivate(stream, "state", $streamReadable); + this.$pull = () => { + var thisResult = $createFulfilledPromise({ + value: flushed, + done: false, + }); + flushed = undefined; + $readableStreamClose(stream); + stream = undefined; + return thisResult; + }; + } else if (this._pendingRead) { + var read = this._pendingRead; + this._pendingRead = undefined; + $putByIdDirectPrivate(this, "pull", $noopDoneFunction); + $fulfillPromise(read, { value: undefined, done: true }); + } + + $readableStreamClose(stream); +} + +export function onFlushDirectStream() { + var stream = this.$controlledReadableStream; + var reader = $getByIdDirectPrivate(stream, "reader"); + if (!reader || !$isReadableStreamDefaultReader(reader)) { + return; + } + + var _pendingRead = this._pendingRead; + this._pendingRead = undefined; + if (_pendingRead && $isPromise(_pendingRead)) { + var flushed = this.$sink.flush(); + if (flushed?.byteLength) { + this._pendingRead = $getByIdDirectPrivate(stream, "readRequests")?.shift(); + $fulfillPromise(_pendingRead, { value: flushed, done: false }); + } else { + this._pendingRead = _pendingRead; + } + } else if ($getByIdDirectPrivate(stream, "readRequests")?.isNotEmpty()) { + var flushed = this.$sink.flush(); + if (flushed?.byteLength) { + $readableStreamFulfillReadRequest(stream, flushed, false); + } + } else if (this._deferFlush === -1) { + this._deferFlush = 1; + } +} + +export function createTextStream(highWaterMark) { + var sink; + var array = []; + var hasString = false; + var hasBuffer = false; + var rope = ""; + var estimatedLength = $toLength(0); + var capability = $newPromiseCapability(Promise); + var calledDone = false; + + sink = { + start() {}, + write(chunk) { + if (typeof chunk === "string") { + var chunkLength = $toLength(chunk.length); + if (chunkLength > 0) { + rope += chunk; + hasString = true; + // TODO: utf16 byte length + estimatedLength += chunkLength; + } + + return chunkLength; + } + + if (!chunk || !($ArrayBuffer.$isView(chunk) || chunk instanceof $ArrayBuffer)) { + $throwTypeError("Expected text, ArrayBuffer or ArrayBufferView"); + } + + const byteLength = $toLength(chunk.byteLength); + if (byteLength > 0) { + hasBuffer = true; + if (rope.length > 0) { + $arrayPush(array, rope, chunk); + rope = ""; + } else { + $arrayPush(array, chunk); + } + } + estimatedLength += byteLength; + return byteLength; + }, + + flush() { + return 0; + }, + + end() { + if (calledDone) { + return ""; + } + return sink.fulfill(); + }, + + fulfill() { + calledDone = true; + const result = sink.finishInternal(); + + $fulfillPromise(capability.$promise, result); + return result; + }, + + finishInternal() { + if (!hasString && !hasBuffer) { + return ""; + } + + if (hasString && !hasBuffer) { + return rope; + } + + if (hasBuffer && !hasString) { + return new globalThis.TextDecoder().decode($Bun.concatArrayBuffers(array)); + } + + // worst case: mixed content + + var arrayBufferSink = new $Bun.ArrayBufferSink(); + arrayBufferSink.start({ + highWaterMark: estimatedLength, + asUint8Array: true, + }); + for (let item of array) { + arrayBufferSink.write(item); + } + array.length = 0; + if (rope.length > 0) { + arrayBufferSink.write(rope); + rope = ""; + } + + // TODO: use builtin + return new globalThis.TextDecoder().decode(arrayBufferSink.end()); + }, + + close() { + try { + if (!calledDone) { + calledDone = true; + sink.fulfill(); + } + } catch (e) {} + }, + }; + + return [sink, capability]; +} + +export function initializeTextStream(underlyingSource, highWaterMark) { + var [sink, closingPromise] = $createTextStream(highWaterMark); + + var controller = { + $underlyingSource: underlyingSource, + $pull: $onPullDirectStream, + $controlledReadableStream: this, + $sink: sink, + close: $onCloseDirectStream, + write: sink.write, + error: $handleDirectStreamError, + end: $onCloseDirectStream, + $close: $onCloseDirectStream, + flush: $onFlushDirectStream, + _pendingRead: undefined, + _deferClose: 0, + _deferFlush: 0, + _deferCloseReason: undefined, + _handleError: undefined, + }; + + $putByIdDirectPrivate(this, "readableStreamController", controller); + $putByIdDirectPrivate(this, "underlyingSource", undefined); + $putByIdDirectPrivate(this, "start", undefined); + return closingPromise; +} + +export function initializeArrayStream(underlyingSource, highWaterMark) { + var array = []; + var closingPromise = $newPromiseCapability(Promise); + var calledDone = false; + + function fulfill() { + calledDone = true; + closingPromise.$resolve.$call(undefined, array); + return array; + } + + var sink = { + start() {}, + write(chunk) { + $arrayPush(array, chunk); + return chunk.byteLength || chunk.length; + }, + + flush() { + return 0; + }, + + end() { + if (calledDone) { + return []; + } + return fulfill(); + }, + + close() { + if (!calledDone) { + fulfill(); + } + }, + }; + + var controller = { + $underlyingSource: underlyingSource, + $pull: $onPullDirectStream, + $controlledReadableStream: this, + $sink: sink, + close: $onCloseDirectStream, + write: sink.write, + error: $handleDirectStreamError, + end: $onCloseDirectStream, + $close: $onCloseDirectStream, + flush: $onFlushDirectStream, + _pendingRead: undefined, + _deferClose: 0, + _deferFlush: 0, + _deferCloseReason: undefined, + _handleError: undefined, + }; + + $putByIdDirectPrivate(this, "readableStreamController", controller); + $putByIdDirectPrivate(this, "underlyingSource", undefined); + $putByIdDirectPrivate(this, "start", undefined); + return closingPromise; +} + +export function initializeArrayBufferStream(underlyingSource, highWaterMark) { + // This is the fallback implementation for direct streams + // When we don't know what the destination type is + // We assume it is a Uint8Array. + + var opts = + highWaterMark && typeof highWaterMark === "number" + ? { highWaterMark, stream: true, asUint8Array: true } + : { stream: true, asUint8Array: true }; + var sink = new $Bun.ArrayBufferSink(); + sink.start(opts); + + var controller = { + $underlyingSource: underlyingSource, + $pull: $onPullDirectStream, + $controlledReadableStream: this, + $sink: sink, + close: $onCloseDirectStream, + write: sink.write.bind(sink), + error: $handleDirectStreamError, + end: $onCloseDirectStream, + $close: $onCloseDirectStream, + flush: $onFlushDirectStream, + _pendingRead: undefined, + _deferClose: 0, + _deferFlush: 0, + _deferCloseReason: undefined, + _handleError: undefined, + }; + + $putByIdDirectPrivate(this, "readableStreamController", controller); + $putByIdDirectPrivate(this, "underlyingSource", undefined); + $putByIdDirectPrivate(this, "start", undefined); +} + +export function readableStreamError(stream, error) { + $assert($isReadableStream(stream)); + $assert($getByIdDirectPrivate(stream, "state") === $streamReadable); + $putByIdDirectPrivate(stream, "state", $streamErrored); + $putByIdDirectPrivate(stream, "storedError", error); + + const reader = $getByIdDirectPrivate(stream, "reader"); + + if (!reader) return; + + if ($isReadableStreamDefaultReader(reader)) { + const requests = $getByIdDirectPrivate(reader, "readRequests"); + $putByIdDirectPrivate(reader, "readRequests", $createFIFO()); + for (var request = requests.shift(); request; request = requests.shift()) $rejectPromise(request, error); + } else { + $assert($isReadableStreamBYOBReader(reader)); + const requests = $getByIdDirectPrivate(reader, "readIntoRequests"); + $putByIdDirectPrivate(reader, "readIntoRequests", $createFIFO()); + for (var request = requests.shift(); request; request = requests.shift()) $rejectPromise(request, error); + } + + $getByIdDirectPrivate(reader, "closedPromiseCapability").$reject.$call(undefined, error); + const promise = $getByIdDirectPrivate(reader, "closedPromiseCapability").$promise; + $markPromiseAsHandled(promise); +} + +export function readableStreamDefaultControllerShouldCallPull(controller) { + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + + if (!$readableStreamDefaultControllerCanCloseOrEnqueue(controller)) return false; + if (!($getByIdDirectPrivate(controller, "started") === 1)) return false; + if ( + (!$isReadableStreamLocked(stream) || + !$getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests")?.isNotEmpty()) && + $readableStreamDefaultControllerGetDesiredSize(controller) <= 0 + ) + return false; + const desiredSize = $readableStreamDefaultControllerGetDesiredSize(controller); + $assert(desiredSize !== null); + return desiredSize > 0; +} + +export function readableStreamDefaultControllerCallPullIfNeeded(controller) { + // FIXME: use $readableStreamDefaultControllerShouldCallPull + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + + if (!$readableStreamDefaultControllerCanCloseOrEnqueue(controller)) return; + if (!($getByIdDirectPrivate(controller, "started") === 1)) return; + if ( + (!$isReadableStreamLocked(stream) || + !$getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests")?.isNotEmpty()) && + $readableStreamDefaultControllerGetDesiredSize(controller) <= 0 + ) + return; + + if ($getByIdDirectPrivate(controller, "pulling")) { + $putByIdDirectPrivate(controller, "pullAgain", true); + return; + } + + $assert(!$getByIdDirectPrivate(controller, "pullAgain")); + $putByIdDirectPrivate(controller, "pulling", true); + + $getByIdDirectPrivate(controller, "pullAlgorithm") + .$call(undefined) + .$then( + function () { + $putByIdDirectPrivate(controller, "pulling", false); + if ($getByIdDirectPrivate(controller, "pullAgain")) { + $putByIdDirectPrivate(controller, "pullAgain", false); + + $readableStreamDefaultControllerCallPullIfNeeded(controller); + } + }, + function (error) { + $readableStreamDefaultControllerError(controller, error); + }, + ); +} + +export function isReadableStreamLocked(stream) { + $assert($isReadableStream(stream)); + return !!$getByIdDirectPrivate(stream, "reader"); +} + +export function readableStreamDefaultControllerGetDesiredSize(controller) { + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + const state = $getByIdDirectPrivate(stream, "state"); + + if (state === $streamErrored) return null; + if (state === $streamClosed) return 0; + + return $getByIdDirectPrivate(controller, "strategy").highWaterMark - $getByIdDirectPrivate(controller, "queue").size; +} + +export function readableStreamReaderGenericCancel(reader, reason) { + const stream = $getByIdDirectPrivate(reader, "ownerReadableStream"); + $assert(!!stream); + return $readableStreamCancel(stream, reason); +} + +export function readableStreamCancel(stream, reason) { + $putByIdDirectPrivate(stream, "disturbed", true); + const state = $getByIdDirectPrivate(stream, "state"); + if (state === $streamClosed) return Promise.$resolve(); + if (state === $streamErrored) return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); + $readableStreamClose(stream); + + var controller = $getByIdDirectPrivate(stream, "readableStreamController"); + var cancel = controller.$cancel; + if (cancel) { + return cancel(controller, reason).$then(function () {}); + } + + var close = controller.close; + if (close) { + return Promise.$resolve(controller.close(reason)); + } + + $throwTypeError("ReadableStreamController has no cancel or close method"); +} + +export function readableStreamDefaultControllerCancel(controller, reason) { + $putByIdDirectPrivate(controller, "queue", $newQueue()); + return $getByIdDirectPrivate(controller, "cancelAlgorithm").$call(undefined, reason); +} + +export function readableStreamDefaultControllerPull(controller) { + var queue = $getByIdDirectPrivate(controller, "queue"); + if (queue.content.isNotEmpty()) { + const chunk = $dequeueValue(queue); + if ($getByIdDirectPrivate(controller, "closeRequested") && queue.content.isEmpty()) + $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); + else $readableStreamDefaultControllerCallPullIfNeeded(controller); + + return $createFulfilledPromise({ value: chunk, done: false }); + } + const pendingPromise = $readableStreamAddReadRequest($getByIdDirectPrivate(controller, "controlledReadableStream")); + $readableStreamDefaultControllerCallPullIfNeeded(controller); + return pendingPromise; +} + +export function readableStreamDefaultControllerClose(controller) { + $assert($readableStreamDefaultControllerCanCloseOrEnqueue(controller)); + $putByIdDirectPrivate(controller, "closeRequested", true); + if ($getByIdDirectPrivate(controller, "queue")?.content?.isEmpty()) + $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); +} + +export function readableStreamClose(stream) { + $assert($getByIdDirectPrivate(stream, "state") === $streamReadable); + $putByIdDirectPrivate(stream, "state", $streamClosed); + if (!$getByIdDirectPrivate(stream, "reader")) return; + + if ($isReadableStreamDefaultReader($getByIdDirectPrivate(stream, "reader"))) { + const requests = $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests"); + if (requests.isNotEmpty()) { + $putByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests", $createFIFO()); + + for (var request = requests.shift(); request; request = requests.shift()) + $fulfillPromise(request, { value: undefined, done: true }); + } + } + + $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "closedPromiseCapability").$resolve.$call(); +} + +export function readableStreamFulfillReadRequest(stream, chunk, done) { + const readRequest = $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests").shift(); + $fulfillPromise(readRequest, { value: chunk, done: done }); +} + +export function readableStreamDefaultControllerEnqueue(controller, chunk) { + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + // this is checked by callers + $assert($readableStreamDefaultControllerCanCloseOrEnqueue(controller)); + + if ( + $isReadableStreamLocked(stream) && + $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests")?.isNotEmpty() + ) { + $readableStreamFulfillReadRequest(stream, chunk, false); + $readableStreamDefaultControllerCallPullIfNeeded(controller); + return; + } + + try { + let chunkSize = 1; + if ($getByIdDirectPrivate(controller, "strategy").size !== undefined) + chunkSize = $getByIdDirectPrivate(controller, "strategy").size(chunk); + $enqueueValueWithSize($getByIdDirectPrivate(controller, "queue"), chunk, chunkSize); + } catch (error) { + $readableStreamDefaultControllerError(controller, error); + throw error; + } + $readableStreamDefaultControllerCallPullIfNeeded(controller); +} + +export function readableStreamDefaultReaderRead(reader) { + const stream = $getByIdDirectPrivate(reader, "ownerReadableStream"); + $assert(!!stream); + const state = $getByIdDirectPrivate(stream, "state"); + + $putByIdDirectPrivate(stream, "disturbed", true); + if (state === $streamClosed) return $createFulfilledPromise({ value: undefined, done: true }); + if (state === $streamErrored) return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); + $assert(state === $streamReadable); + + return $getByIdDirectPrivate(stream, "readableStreamController").$pull( + $getByIdDirectPrivate(stream, "readableStreamController"), + ); +} + +export function readableStreamAddReadRequest(stream) { + $assert($isReadableStreamDefaultReader($getByIdDirectPrivate(stream, "reader"))); + $assert($getByIdDirectPrivate(stream, "state") == $streamReadable); + + const readRequest = $newPromise(); + + $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests").push(readRequest); + + return readRequest; +} + +export function isReadableStreamDisturbed(stream) { + $assert($isReadableStream(stream)); + return $getByIdDirectPrivate(stream, "disturbed"); +} + +export function readableStreamReaderGenericRelease(reader) { + $assert(!!$getByIdDirectPrivate(reader, "ownerReadableStream")); + $assert($getByIdDirectPrivate($getByIdDirectPrivate(reader, "ownerReadableStream"), "reader") === reader); + + if ($getByIdDirectPrivate($getByIdDirectPrivate(reader, "ownerReadableStream"), "state") === $streamReadable) + $getByIdDirectPrivate(reader, "closedPromiseCapability").$reject.$call( + undefined, + $makeTypeError("releasing lock of reader whose stream is still in readable state"), + ); + else + $putByIdDirectPrivate(reader, "closedPromiseCapability", { + $promise: $newHandledRejectedPromise($makeTypeError("reader released lock")), + }); + + const promise = $getByIdDirectPrivate(reader, "closedPromiseCapability").$promise; + $markPromiseAsHandled(promise); + $putByIdDirectPrivate($getByIdDirectPrivate(reader, "ownerReadableStream"), "reader", undefined); + $putByIdDirectPrivate(reader, "ownerReadableStream", undefined); +} + +export function readableStreamDefaultControllerCanCloseOrEnqueue(controller) { + return ( + !$getByIdDirectPrivate(controller, "closeRequested") && + $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === $streamReadable + ); +} + +export function lazyLoadStream(stream, autoAllocateChunkSize) { + var nativeType = $getByIdDirectPrivate(stream, "bunNativeType"); + var nativePtr = $getByIdDirectPrivate(stream, "bunNativePtr"); + var Prototype = $lazyStreamPrototypeMap.$get(nativeType); + if (Prototype === undefined) { + var [pull, start, cancel, setClose, deinit, setRefOrUnref, drain] = $lazyLoad(nativeType); + var closer = [false]; + var handleResult; + function handleNativeReadableStreamPromiseResult(val) { + var { c, v } = this; + this.c = undefined; + this.v = undefined; + handleResult(val, c, v); + } + + function callClose(controller) { + try { + controller.close(); + } catch (e) { + globalThis.reportError(e); + } + } + + handleResult = function handleResult(result, controller, view) { + if (result && $isPromise(result)) { + return result.then( + handleNativeReadableStreamPromiseResult.bind({ + c: controller, + v: view, + }), + err => controller.error(err), + ); + } else if (typeof result === "number") { + if (view && view.byteLength === result && view.buffer === controller.byobRequest?.view?.buffer) { + controller.byobRequest.respondWithNewView(view); + } else { + controller.byobRequest.respond(result); + } + } else if (result.constructor === $Uint8Array) { + controller.enqueue(result); + } + + if (closer[0] || result === false) { + $enqueueJob(callClose, controller); + closer[0] = false; + } + }; + + function createResult(tag, controller, view, closer) { + closer[0] = false; + + var result; + try { + result = pull(tag, view, closer); + } catch (err) { + return controller.error(err); + } + + return handleResult(result, controller, view); + } + + const registry = deinit ? new FinalizationRegistry(deinit) : null; + Prototype = class NativeReadableStreamSource { + constructor(tag, autoAllocateChunkSize, drainValue) { + this.#tag = tag; + this.#cancellationToken = {}; + this.pull = this.#pull.bind(this); + this.cancel = this.#cancel.bind(this); + this.autoAllocateChunkSize = autoAllocateChunkSize; + + if (drainValue !== undefined) { + this.start = controller => { + controller.enqueue(drainValue); + }; + } + + if (registry) { + registry.register(this, tag, this.#cancellationToken); + } + } + + #cancellationToken; + pull; + cancel; + start; + + #tag; + type = "bytes"; + autoAllocateChunkSize = 0; + + static startSync = start; + + #pull(controller) { + var tag = this.#tag; + + if (!tag) { + controller.close(); + return; + } + + createResult(tag, controller, controller.byobRequest.view, closer); + } + + #cancel(reason) { + var tag = this.#tag; + + registry && registry.unregister(this.#cancellationToken); + setRefOrUnref && setRefOrUnref(tag, false); + cancel(tag, reason); + } + static deinit = deinit; + static drain = drain; + }; + $lazyStreamPrototypeMap.$set(nativeType, Prototype); + } + + const chunkSize = Prototype.startSync(nativePtr, autoAllocateChunkSize); + var drainValue; + const { drain: drainFn, deinit: deinitFn } = Prototype; + if (drainFn) { + drainValue = drainFn(nativePtr); + } + + // empty file, no need for native back-and-forth on this + if (chunkSize === 0) { + deinit && nativePtr && $enqueueJob(deinit, nativePtr); + + if ((drainValue?.byteLength ?? 0) > 0) { + return { + start(controller) { + controller.enqueue(drainValue); + controller.close(); + }, + type: "bytes", + }; + } + + return { + start(controller) { + controller.close(); + }, + type: "bytes", + }; + } + + return new Prototype(nativePtr, chunkSize, drainValue); +} + +export function readableStreamIntoArray(stream) { + var reader = stream.getReader(); + var manyResult = reader.readMany(); + + async function processManyResult(result) { + if (result.done) { + return []; + } + + var chunks = result.value || []; + + while (true) { + var thisResult = await reader.read(); + if (thisResult.done) { + break; + } + chunks = chunks.concat(thisResult.value); + } + + return chunks; + } + + if (manyResult && $isPromise(manyResult)) { + return manyResult.$then(processManyResult); + } + + return processManyResult(manyResult); +} + +export function readableStreamIntoText(stream) { + const [textStream, closer] = $createTextStream($getByIdDirectPrivate(stream, "highWaterMark")); + const prom = $readStreamIntoSink(stream, textStream, false); + if (prom && $isPromise(prom)) { + return Promise.$resolve(prom).$then(closer.$promise); + } + return closer.$promise; +} + +export function readableStreamToArrayBufferDirect(stream, underlyingSource) { + var sink = new $Bun.ArrayBufferSink(); + $putByIdDirectPrivate(stream, "underlyingSource", undefined); + var highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark"); + sink.start(highWaterMark ? { highWaterMark } : {}); + var capability = $newPromiseCapability(Promise); + var ended = false; + var pull = underlyingSource.pull; + var close = underlyingSource.close; + + var controller = { + start() {}, + close(reason) { + if (!ended) { + ended = true; + if (close) { + close(); + } + + $fulfillPromise(capability.$promise, sink.end()); + } + }, + end() { + if (!ended) { + ended = true; + if (close) { + close(); + } + $fulfillPromise(capability.$promise, sink.end()); + } + }, + flush() { + return 0; + }, + write: sink.write.bind(sink), + }; + + var didError = false; + try { + const firstPull = pull(controller); + if (firstPull && $isObject(firstPull) && $isPromise(firstPull)) { + return (async function (controller, promise, pull) { + while (!ended) { + await pull(controller); + } + return await promise; + })(controller, promise, pull); + } + + return capability.$promise; + } catch (e) { + didError = true; + $readableStreamError(stream, e); + return Promise.$reject(e); + } finally { + if (!didError && stream) $readableStreamClose(stream); + controller = close = sink = pull = stream = undefined; + } +} + +export async function readableStreamToTextDirect(stream, underlyingSource) { + const capability = $initializeTextStream.$call(stream, underlyingSource, undefined); + var reader = stream.getReader(); + + while ($getByIdDirectPrivate(stream, "state") === $streamReadable) { + var thisResult = await reader.read(); + if (thisResult.done) { + break; + } + } + + try { + reader.releaseLock(); + } catch (e) {} + reader = undefined; + stream = undefined; + + return capability.$promise; +} + +export async function readableStreamToArrayDirect(stream, underlyingSource) { + const capability = $initializeArrayStream.$call(stream, underlyingSource, undefined); + underlyingSource = undefined; + var reader = stream.getReader(); + try { + while ($getByIdDirectPrivate(stream, "state") === $streamReadable) { + var thisResult = await reader.read(); + if (thisResult.done) { + break; + } + } + + try { + reader.releaseLock(); + } catch (e) {} + reader = undefined; + + return Promise.$resolve(capability.$promise); + } catch (e) { + throw e; + } finally { + stream = undefined; + reader = undefined; + } +} + +export function readableStreamDefineLazyIterators(prototype) { + var asyncIterator = globalThis.Symbol.asyncIterator; + + var ReadableStreamAsyncIterator = async function* ReadableStreamAsyncIterator(stream, preventCancel) { + var reader = stream.getReader(); + var deferredError; + try { + while (true) { + var done, value; + const firstResult = reader.readMany(); + if ($isPromise(firstResult)) { + ({ done, value } = await firstResult); + } else { + ({ done, value } = firstResult); + } + + if (done) { + return; + } + yield* value; + } + } catch (e) { + deferredError = e; + } finally { + reader.releaseLock(); + + if (!preventCancel) { + stream.cancel(deferredError); + } + + if (deferredError) { + throw deferredError; + } + } + }; + var createAsyncIterator = function asyncIterator() { + return ReadableStreamAsyncIterator(this, false); + }; + var createValues = function values({ preventCancel = false } = { preventCancel: false }) { + return ReadableStreamAsyncIterator(this, preventCancel); + }; + $Object.$defineProperty(prototype, asyncIterator, { value: createAsyncIterator }); + $Object.$defineProperty(prototype, "values", { value: createValues }); + return prototype; +} diff --git a/src/js/builtins/StreamInternals.ts b/src/js/builtins/StreamInternals.ts new file mode 100644 index 000000000..b42dc2f57 --- /dev/null +++ b/src/js/builtins/StreamInternals.ts @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2015 Canon Inc. + * Copyright (C) 2015 Igalia. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// @internal + +export function markPromiseAsHandled(promise: Promise<unknown>) { + $assert($isPromise(promise)); + $putPromiseInternalField( + promise, + $promiseFieldFlags, + $getPromiseInternalField(promise, $promiseFieldFlags) | $promiseFlagsIsHandled, + ); +} + +export function shieldingPromiseResolve(result) { + const promise = Promise.$resolve(result); + if (promise.$then === undefined) promise.$then = Promise.prototype.$then; + return promise; +} + +export function promiseInvokeOrNoopMethodNoCatch(object, method, args) { + if (method === undefined) return Promise.$resolve(); + return $shieldingPromiseResolve(method.$apply(object, args)); +} + +export function promiseInvokeOrNoopNoCatch(object, key, args) { + return $promiseInvokeOrNoopMethodNoCatch(object, object[key], args); +} + +export function promiseInvokeOrNoopMethod(object, method, args) { + try { + return $promiseInvokeOrNoopMethodNoCatch(object, method, args); + } catch (error) { + return Promise.$reject(error); + } +} + +export function promiseInvokeOrNoop(object, key, args) { + try { + return $promiseInvokeOrNoopNoCatch(object, key, args); + } catch (error) { + return Promise.$reject(error); + } +} + +export function promiseInvokeOrFallbackOrNoop(object, key1, args1, key2, args2) { + try { + const method = object[key1]; + if (method === undefined) return $promiseInvokeOrNoopNoCatch(object, key2, args2); + return $shieldingPromiseResolve(method.$apply(object, args1)); + } catch (error) { + return Promise.$reject(error); + } +} + +export function validateAndNormalizeQueuingStrategy(size, highWaterMark) { + if (size !== undefined && typeof size !== "function") throw new TypeError("size parameter must be a function"); + + const newHighWaterMark = $toNumber(highWaterMark); + + if (isNaN(newHighWaterMark) || newHighWaterMark < 0) + throw new RangeError("highWaterMark value is negative or not a number"); + + return { size: size, highWaterMark: newHighWaterMark }; +} + +$linkTimeConstant; +export function createFIFO() { + var slice = Array.prototype.slice; + + class Denqueue { + constructor() { + this._head = 0; + this._tail = 0; + // this._capacity = 0; + this._capacityMask = 0x3; + this._list = $newArrayWithSize(4); + } + + _head; + _tail; + _capacityMask; + _list; + + size() { + if (this._head === this._tail) return 0; + if (this._head < this._tail) return this._tail - this._head; + else return this._capacityMask + 1 - (this._head - this._tail); + } + + isEmpty() { + return this.size() == 0; + } + + isNotEmpty() { + return this.size() > 0; + } + + shift() { + var { _head: head, _tail, _list, _capacityMask } = this; + if (head === _tail) return undefined; + var item = _list[head]; + $putByValDirect(_list, head, undefined); + head = this._head = (head + 1) & _capacityMask; + if (head < 2 && _tail > 10000 && _tail <= _list.length >>> 2) this._shrinkArray(); + return item; + } + + peek() { + if (this._head === this._tail) return undefined; + return this._list[this._head]; + } + + push(item) { + var tail = this._tail; + $putByValDirect(this._list, tail, item); + this._tail = (tail + 1) & this._capacityMask; + if (this._tail === this._head) { + this._growArray(); + } + // if (this._capacity && this.size() > this._capacity) { + // this.shift(); + // } + } + + toArray(fullCopy) { + var list = this._list; + var len = $toLength(list.length); + + if (fullCopy || this._head > this._tail) { + var _head = $toLength(this._head); + var _tail = $toLength(this._tail); + var total = $toLength(len - _head + _tail); + var array = $newArrayWithSize(total); + var j = 0; + for (var i = _head; i < len; i++) $putByValDirect(array, j++, list[i]); + for (var i = 0; i < _tail; i++) $putByValDirect(array, j++, list[i]); + return array; + } else { + return slice.$call(list, this._head, this._tail); + } + } + + clear() { + this._head = 0; + this._tail = 0; + this._list.fill(undefined); + } + + _growArray() { + if (this._head) { + // copy existing data, head to end, then beginning to tail. + this._list = this.toArray(true); + this._head = 0; + } + + // head is at 0 and array is now full, safe to extend + this._tail = $toLength(this._list.length); + + this._list.length <<= 1; + this._capacityMask = (this._capacityMask << 1) | 1; + } + + shrinkArray() { + this._list.length >>>= 1; + this._capacityMask >>>= 1; + } + } + + return new Denqueue(); +} + +export function newQueue() { + return { content: $createFIFO(), size: 0 }; +} + +export function dequeueValue(queue) { + const record = queue.content.shift(); + queue.size -= record.size; + // As described by spec, below case may occur due to rounding errors. + if (queue.size < 0) queue.size = 0; + return record.value; +} + +export function enqueueValueWithSize(queue, value, size) { + size = $toNumber(size); + if (!isFinite(size) || size < 0) throw new RangeError("size has an incorrect value"); + + queue.content.push({ value, size }); + queue.size += size; +} + +export function peekQueueValue(queue) { + return queue.content.peek()?.value; +} + +export function resetQueue(queue) { + $assert("content" in queue); + $assert("size" in queue); + queue.content.clear(); + queue.size = 0; +} + +export function extractSizeAlgorithm(strategy) { + const sizeAlgorithm = strategy.size; + + if (sizeAlgorithm === undefined) return () => 1; + + if (typeof sizeAlgorithm !== "function") throw new TypeError("strategy.size must be a function"); + + return chunk => { + return sizeAlgorithm(chunk); + }; +} + +export function extractHighWaterMark(strategy, defaultHWM) { + const highWaterMark = strategy.highWaterMark; + + if (highWaterMark === undefined) return defaultHWM; + + if (isNaN(highWaterMark) || highWaterMark < 0) + throw new RangeError("highWaterMark value is negative or not a number"); + + return $toNumber(highWaterMark); +} + +export function extractHighWaterMarkFromQueuingStrategyInit(init: { highWaterMark?: number }) { + if (!$isObject(init)) throw new TypeError("QueuingStrategyInit argument must be an object."); + const { highWaterMark } = init; + if (highWaterMark === undefined) throw new TypeError("QueuingStrategyInit.highWaterMark member is required."); + + return $toNumber(highWaterMark); +} + +export function createFulfilledPromise(value) { + const promise = $newPromise(); + $fulfillPromise(promise, value); + return promise; +} + +export function toDictionary(value, defaultValue, errorMessage) { + if (value === undefined || value === null) return defaultValue; + if (!$isObject(value)) throw new TypeError(errorMessage); + return value; +} diff --git a/src/js/builtins/TransformStream.ts b/src/js/builtins/TransformStream.ts new file mode 100644 index 000000000..54467db39 --- /dev/null +++ b/src/js/builtins/TransformStream.ts @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2020 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeTransformStream(this) { + let transformer = arguments[0]; + + // This is the path for CreateTransformStream. + if ($isObject(transformer) && $getByIdDirectPrivate(transformer, "TransformStream")) return this; + + let writableStrategy = arguments[1]; + let readableStrategy = arguments[2]; + + if (transformer === undefined) transformer = null; + + if (readableStrategy === undefined) readableStrategy = {}; + + if (writableStrategy === undefined) writableStrategy = {}; + + let transformerDict = {}; + if (transformer !== null) { + if ("start" in transformer) { + transformerDict["start"] = transformer["start"]; + if (typeof transformerDict["start"] !== "function") $throwTypeError("transformer.start should be a function"); + } + if ("transform" in transformer) { + transformerDict["transform"] = transformer["transform"]; + if (typeof transformerDict["transform"] !== "function") + $throwTypeError("transformer.transform should be a function"); + } + if ("flush" in transformer) { + transformerDict["flush"] = transformer["flush"]; + if (typeof transformerDict["flush"] !== "function") $throwTypeError("transformer.flush should be a function"); + } + + if ("readableType" in transformer) throw new RangeError("TransformStream transformer has a readableType"); + if ("writableType" in transformer) throw new RangeError("TransformStream transformer has a writableType"); + } + + const readableHighWaterMark = $extractHighWaterMark(readableStrategy, 0); + const readableSizeAlgorithm = $extractSizeAlgorithm(readableStrategy); + + const writableHighWaterMark = $extractHighWaterMark(writableStrategy, 1); + const writableSizeAlgorithm = $extractSizeAlgorithm(writableStrategy); + + const startPromiseCapability = $newPromiseCapability(Promise); + $initializeTransformStream( + this, + startPromiseCapability.$promise, + writableHighWaterMark, + writableSizeAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, + ); + $setUpTransformStreamDefaultControllerFromTransformer(this, transformer, transformerDict); + + if ("start" in transformerDict) { + const controller = $getByIdDirectPrivate(this, "controller"); + const startAlgorithm = () => $promiseInvokeOrNoopMethodNoCatch(transformer, transformerDict["start"], [controller]); + startAlgorithm().$then( + () => { + // FIXME: We probably need to resolve start promise with the result of the start algorithm. + startPromiseCapability.$resolve.$call(); + }, + error => { + startPromiseCapability.$reject.$call(undefined, error); + }, + ); + } else startPromiseCapability.$resolve.$call(); + + return this; +} + +$getter; +export function readable() { + if (!$isTransformStream(this)) throw $makeThisTypeError("TransformStream", "readable"); + + return $getByIdDirectPrivate(this, "readable"); +} + +export function writable() { + if (!$isTransformStream(this)) throw $makeThisTypeError("TransformStream", "writable"); + + return $getByIdDirectPrivate(this, "writable"); +} diff --git a/src/js/builtins/TransformStreamDefaultController.ts b/src/js/builtins/TransformStreamDefaultController.ts new file mode 100644 index 000000000..1045498b8 --- /dev/null +++ b/src/js/builtins/TransformStreamDefaultController.ts @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeTransformStreamDefaultController(this) { + return this; +} + +$getter; +export function desiredSize(this) { + if (!$isTransformStreamDefaultController(this)) + throw $makeThisTypeError("TransformStreamDefaultController", "enqueue"); + + const stream = $getByIdDirectPrivate(this, "stream"); + const readable = $getByIdDirectPrivate(stream, "readable"); + const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); + + return $readableStreamDefaultControllerGetDesiredSize(readableController); +} + +export function enqueue(this, chunk) { + if (!$isTransformStreamDefaultController(this)) + throw $makeThisTypeError("TransformStreamDefaultController", "enqueue"); + + $transformStreamDefaultControllerEnqueue(this, chunk); +} + +export function error(this, e) { + if (!$isTransformStreamDefaultController(this)) throw $makeThisTypeError("TransformStreamDefaultController", "error"); + + $transformStreamDefaultControllerError(this, e); +} + +export function terminate(this) { + if (!$isTransformStreamDefaultController(this)) + throw $makeThisTypeError("TransformStreamDefaultController", "terminate"); + + $transformStreamDefaultControllerTerminate(this); +} diff --git a/src/js/builtins/TransformStreamInternals.ts b/src/js/builtins/TransformStreamInternals.ts new file mode 100644 index 000000000..9994d1282 --- /dev/null +++ b/src/js/builtins/TransformStreamInternals.ts @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2020 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +// @internal + +export function isTransformStream(stream) { + return $isObject(stream) && !!$getByIdDirectPrivate(stream, "readable"); +} + +export function isTransformStreamDefaultController(controller) { + return $isObject(controller) && !!$getByIdDirectPrivate(controller, "transformAlgorithm"); +} + +export function createTransformStream( + startAlgorithm, + transformAlgorithm, + flushAlgorithm, + writableHighWaterMark, + writableSizeAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, +) { + if (writableHighWaterMark === undefined) writableHighWaterMark = 1; + if (writableSizeAlgorithm === undefined) writableSizeAlgorithm = () => 1; + if (readableHighWaterMark === undefined) readableHighWaterMark = 0; + if (readableSizeAlgorithm === undefined) readableSizeAlgorithm = () => 1; + $assert(writableHighWaterMark >= 0); + $assert(readableHighWaterMark >= 0); + + const transform = {}; + $putByIdDirectPrivate(transform, "TransformStream", true); + + const stream = new TransformStream(transform); + const startPromiseCapability = $newPromiseCapability(Promise); + $initializeTransformStream( + stream, + startPromiseCapability.$promise, + writableHighWaterMark, + writableSizeAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, + ); + + const controller = new TransformStreamDefaultController(); + $setUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm); + + startAlgorithm().$then( + () => { + startPromiseCapability.$resolve.$call(); + }, + error => { + startPromiseCapability.$reject.$call(undefined, error); + }, + ); + + return stream; +} + +export function initializeTransformStream( + stream, + startPromise, + writableHighWaterMark, + writableSizeAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, +) { + const startAlgorithm = () => { + return startPromise; + }; + const writeAlgorithm = chunk => { + return $transformStreamDefaultSinkWriteAlgorithm(stream, chunk); + }; + const abortAlgorithm = reason => { + return $transformStreamDefaultSinkAbortAlgorithm(stream, reason); + }; + const closeAlgorithm = () => { + return $transformStreamDefaultSinkCloseAlgorithm(stream); + }; + const writable = $createWritableStream( + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + writableHighWaterMark, + writableSizeAlgorithm, + ); + + const pullAlgorithm = () => { + return $transformStreamDefaultSourcePullAlgorithm(stream); + }; + const cancelAlgorithm = reason => { + $transformStreamErrorWritableAndUnblockWrite(stream, reason); + return Promise.$resolve(); + }; + const underlyingSource = {}; + $putByIdDirectPrivate(underlyingSource, "start", startAlgorithm); + $putByIdDirectPrivate(underlyingSource, "pull", pullAlgorithm); + $putByIdDirectPrivate(underlyingSource, "cancel", cancelAlgorithm); + const options = {}; + $putByIdDirectPrivate(options, "size", readableSizeAlgorithm); + $putByIdDirectPrivate(options, "highWaterMark", readableHighWaterMark); + const readable = new ReadableStream(underlyingSource, options); + + // The writable to expose to JS through writable getter. + $putByIdDirectPrivate(stream, "writable", writable); + // The writable to use for the actual transform algorithms. + $putByIdDirectPrivate(stream, "internalWritable", $getInternalWritableStream(writable)); + + $putByIdDirectPrivate(stream, "readable", readable); + $putByIdDirectPrivate(stream, "backpressure", undefined); + $putByIdDirectPrivate(stream, "backpressureChangePromise", undefined); + + $transformStreamSetBackpressure(stream, true); + $putByIdDirectPrivate(stream, "controller", undefined); +} + +export function transformStreamError(stream, e) { + const readable = $getByIdDirectPrivate(stream, "readable"); + const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); + $readableStreamDefaultControllerError(readableController, e); + + $transformStreamErrorWritableAndUnblockWrite(stream, e); +} + +export function transformStreamErrorWritableAndUnblockWrite(stream, e) { + $transformStreamDefaultControllerClearAlgorithms($getByIdDirectPrivate(stream, "controller")); + + const writable = $getByIdDirectPrivate(stream, "internalWritable"); + $writableStreamDefaultControllerErrorIfNeeded($getByIdDirectPrivate(writable, "controller"), e); + + if ($getByIdDirectPrivate(stream, "backpressure")) $transformStreamSetBackpressure(stream, false); +} + +export function transformStreamSetBackpressure(stream, backpressure) { + $assert($getByIdDirectPrivate(stream, "backpressure") !== backpressure); + + const backpressureChangePromise = $getByIdDirectPrivate(stream, "backpressureChangePromise"); + if (backpressureChangePromise !== undefined) backpressureChangePromise.$resolve.$call(); + + $putByIdDirectPrivate(stream, "backpressureChangePromise", $newPromiseCapability(Promise)); + $putByIdDirectPrivate(stream, "backpressure", backpressure); +} + +export function setUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm) { + $assert($isTransformStream(stream)); + $assert($getByIdDirectPrivate(stream, "controller") === undefined); + + $putByIdDirectPrivate(controller, "stream", stream); + $putByIdDirectPrivate(stream, "controller", controller); + $putByIdDirectPrivate(controller, "transformAlgorithm", transformAlgorithm); + $putByIdDirectPrivate(controller, "flushAlgorithm", flushAlgorithm); +} + +export function setUpTransformStreamDefaultControllerFromTransformer(stream, transformer, transformerDict) { + const controller = new TransformStreamDefaultController(); + let transformAlgorithm = chunk => { + try { + $transformStreamDefaultControllerEnqueue(controller, chunk); + } catch (e) { + return Promise.$reject(e); + } + return Promise.$resolve(); + }; + let flushAlgorithm = () => { + return Promise.$resolve(); + }; + + if ("transform" in transformerDict) + transformAlgorithm = chunk => { + return $promiseInvokeOrNoopMethod(transformer, transformerDict["transform"], [chunk, controller]); + }; + + if ("flush" in transformerDict) { + flushAlgorithm = () => { + return $promiseInvokeOrNoopMethod(transformer, transformerDict["flush"], [controller]); + }; + } + + $setUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm); +} + +export function transformStreamDefaultControllerClearAlgorithms(controller) { + // We set transformAlgorithm to true to allow GC but keep the isTransformStreamDefaultController check. + $putByIdDirectPrivate(controller, "transformAlgorithm", true); + $putByIdDirectPrivate(controller, "flushAlgorithm", undefined); +} + +export function transformStreamDefaultControllerEnqueue(controller, chunk) { + const stream = $getByIdDirectPrivate(controller, "stream"); + const readable = $getByIdDirectPrivate(stream, "readable"); + const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); + + $assert(readableController !== undefined); + if (!$readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) + $throwTypeError("TransformStream.readable cannot close or enqueue"); + + try { + $readableStreamDefaultControllerEnqueue(readableController, chunk); + } catch (e) { + $transformStreamErrorWritableAndUnblockWrite(stream, e); + throw $getByIdDirectPrivate(readable, "storedError"); + } + + const backpressure = !$readableStreamDefaultControllerShouldCallPull(readableController); + if (backpressure !== $getByIdDirectPrivate(stream, "backpressure")) { + $assert(backpressure); + $transformStreamSetBackpressure(stream, true); + } +} + +export function transformStreamDefaultControllerError(controller, e) { + $transformStreamError($getByIdDirectPrivate(controller, "stream"), e); +} + +export function transformStreamDefaultControllerPerformTransform(controller, chunk) { + const promiseCapability = $newPromiseCapability(Promise); + + const transformPromise = $getByIdDirectPrivate(controller, "transformAlgorithm").$call(undefined, chunk); + transformPromise.$then( + () => { + promiseCapability.$resolve(); + }, + r => { + $transformStreamError($getByIdDirectPrivate(controller, "stream"), r); + promiseCapability.$reject.$call(undefined, r); + }, + ); + return promiseCapability.$promise; +} + +export function transformStreamDefaultControllerTerminate(controller) { + const stream = $getByIdDirectPrivate(controller, "stream"); + const readable = $getByIdDirectPrivate(stream, "readable"); + const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); + + // FIXME: Update readableStreamDefaultControllerClose to make this check. + if ($readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) + $readableStreamDefaultControllerClose(readableController); + const error = $makeTypeError("the stream has been terminated"); + $transformStreamErrorWritableAndUnblockWrite(stream, error); +} + +export function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) { + const writable = $getByIdDirectPrivate(stream, "internalWritable"); + + $assert($getByIdDirectPrivate(writable, "state") === "writable"); + + const controller = $getByIdDirectPrivate(stream, "controller"); + + if ($getByIdDirectPrivate(stream, "backpressure")) { + const promiseCapability = $newPromiseCapability(Promise); + + const backpressureChangePromise = $getByIdDirectPrivate(stream, "backpressureChangePromise"); + $assert(backpressureChangePromise !== undefined); + backpressureChangePromise.$promise.$then( + () => { + const state = $getByIdDirectPrivate(writable, "state"); + if (state === "erroring") { + promiseCapability.$reject.$call(undefined, $getByIdDirectPrivate(writable, "storedError")); + return; + } + + $assert(state === "writable"); + $transformStreamDefaultControllerPerformTransform(controller, chunk).$then( + () => { + promiseCapability.$resolve(); + }, + e => { + promiseCapability.$reject.$call(undefined, e); + }, + ); + }, + e => { + promiseCapability.$reject.$call(undefined, e); + }, + ); + + return promiseCapability.$promise; + } + return $transformStreamDefaultControllerPerformTransform(controller, chunk); +} + +export function transformStreamDefaultSinkAbortAlgorithm(stream, reason) { + $transformStreamError(stream, reason); + return Promise.$resolve(); +} + +export function transformStreamDefaultSinkCloseAlgorithm(stream) { + const readable = $getByIdDirectPrivate(stream, "readable"); + const controller = $getByIdDirectPrivate(stream, "controller"); + const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); + + const flushAlgorithm = $getByIdDirectPrivate(controller, "flushAlgorithm"); + $assert(flushAlgorithm !== undefined); + const flushPromise = $getByIdDirectPrivate(controller, "flushAlgorithm").$call(); + $transformStreamDefaultControllerClearAlgorithms(controller); + + const promiseCapability = $newPromiseCapability(Promise); + flushPromise.$then( + () => { + if ($getByIdDirectPrivate(readable, "state") === $streamErrored) { + promiseCapability.$reject.$call(undefined, $getByIdDirectPrivate(readable, "storedError")); + return; + } + + // FIXME: Update readableStreamDefaultControllerClose to make this check. + if ($readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) + $readableStreamDefaultControllerClose(readableController); + promiseCapability.$resolve(); + }, + r => { + $transformStreamError($getByIdDirectPrivate(controller, "stream"), r); + promiseCapability.$reject.$call(undefined, $getByIdDirectPrivate(readable, "storedError")); + }, + ); + return promiseCapability.$promise; +} + +export function transformStreamDefaultSourcePullAlgorithm(stream) { + $assert($getByIdDirectPrivate(stream, "backpressure")); + $assert($getByIdDirectPrivate(stream, "backpressureChangePromise") !== undefined); + + $transformStreamSetBackpressure(stream, false); + + return $getByIdDirectPrivate(stream, "backpressureChangePromise").$promise; +} diff --git a/src/js/builtins/WritableStreamDefaultController.ts b/src/js/builtins/WritableStreamDefaultController.ts new file mode 100644 index 000000000..1a3ddc290 --- /dev/null +++ b/src/js/builtins/WritableStreamDefaultController.ts @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeWritableStreamDefaultController(this) { + $putByIdDirectPrivate(this, "queue", $newQueue()); + $putByIdDirectPrivate(this, "abortSteps", reason => { + const result = $getByIdDirectPrivate(this, "abortAlgorithm").$call(undefined, reason); + $writableStreamDefaultControllerClearAlgorithms(this); + return result; + }); + + $putByIdDirectPrivate(this, "errorSteps", () => { + $resetQueue($getByIdDirectPrivate(this, "queue")); + }); + + return this; +} + +export function error(this, e) { + if ($getByIdDirectPrivate(this, "abortSteps") === undefined) + throw $makeThisTypeError("WritableStreamDefaultController", "error"); + + const stream = $getByIdDirectPrivate(this, "stream"); + if ($getByIdDirectPrivate(stream, "state") !== "writable") return; + $writableStreamDefaultControllerError(this, e); +} diff --git a/src/js/builtins/WritableStreamDefaultWriter.ts b/src/js/builtins/WritableStreamDefaultWriter.ts new file mode 100644 index 000000000..795b43892 --- /dev/null +++ b/src/js/builtins/WritableStreamDefaultWriter.ts @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function initializeWritableStreamDefaultWriter(stream) { + // stream can be a WritableStream if WritableStreamDefaultWriter constructor is called directly from JS + // or an InternalWritableStream in other code paths. + const internalStream = $getInternalWritableStream(stream); + if (internalStream) stream = internalStream; + + if (!$isWritableStream(stream)) $throwTypeError("WritableStreamDefaultWriter constructor takes a WritableStream"); + + $setUpWritableStreamDefaultWriter(this, stream); + return this; +} + +$getter; +export function closed() { + if (!$isWritableStreamDefaultWriter(this)) + return Promise.$reject($makeGetterTypeError("WritableStreamDefaultWriter", "closed")); + + return $getByIdDirectPrivate(this, "closedPromise").$promise; +} + +$getter; +export function desiredSize() { + if (!$isWritableStreamDefaultWriter(this)) throw $makeThisTypeError("WritableStreamDefaultWriter", "desiredSize"); + + if ($getByIdDirectPrivate(this, "stream") === undefined) $throwTypeError("WritableStreamDefaultWriter has no stream"); + + return $writableStreamDefaultWriterGetDesiredSize(this); +} + +$getter; +export function ready() { + if (!$isWritableStreamDefaultWriter(this)) + return Promise.$reject($makeThisTypeError("WritableStreamDefaultWriter", "ready")); + + return $getByIdDirectPrivate(this, "readyPromise").$promise; +} + +export function abort(reason) { + if (!$isWritableStreamDefaultWriter(this)) + return Promise.$reject($makeThisTypeError("WritableStreamDefaultWriter", "abort")); + + if ($getByIdDirectPrivate(this, "stream") === undefined) + return Promise.$reject($makeTypeError("WritableStreamDefaultWriter has no stream")); + + return $writableStreamDefaultWriterAbort(this, reason); +} + +export function close() { + if (!$isWritableStreamDefaultWriter(this)) + return Promise.$reject($makeThisTypeError("WritableStreamDefaultWriter", "close")); + + const stream = $getByIdDirectPrivate(this, "stream"); + if (stream === undefined) return Promise.$reject($makeTypeError("WritableStreamDefaultWriter has no stream")); + + if ($writableStreamCloseQueuedOrInFlight(stream)) + return Promise.$reject($makeTypeError("WritableStreamDefaultWriter is being closed")); + + return $writableStreamDefaultWriterClose(this); +} + +export function releaseLock() { + if (!$isWritableStreamDefaultWriter(this)) throw $makeThisTypeError("WritableStreamDefaultWriter", "releaseLock"); + + const stream = $getByIdDirectPrivate(this, "stream"); + if (stream === undefined) return; + + $assert($getByIdDirectPrivate(stream, "writer") !== undefined); + $writableStreamDefaultWriterRelease(this); +} + +export function write(chunk) { + if (!$isWritableStreamDefaultWriter(this)) + return Promise.$reject($makeThisTypeError("WritableStreamDefaultWriter", "write")); + + if ($getByIdDirectPrivate(this, "stream") === undefined) + return Promise.$reject($makeTypeError("WritableStreamDefaultWriter has no stream")); + + return $writableStreamDefaultWriterWrite(this, chunk); +} diff --git a/src/js/builtins/WritableStreamInternals.ts b/src/js/builtins/WritableStreamInternals.ts new file mode 100644 index 000000000..f436a285e --- /dev/null +++ b/src/js/builtins/WritableStreamInternals.ts @@ -0,0 +1,790 @@ +/* + * Copyright (C) 2015 Canon Inc. + * Copyright (C) 2015 Igalia + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// @internal + +export function isWritableStream(stream) { + return $isObject(stream) && !!$getByIdDirectPrivate(stream, "underlyingSink"); +} + +export function isWritableStreamDefaultWriter(writer) { + return $isObject(writer) && !!$getByIdDirectPrivate(writer, "closedPromise"); +} + +export function acquireWritableStreamDefaultWriter(stream) { + return new WritableStreamDefaultWriter(stream); +} + +// https://streams.spec.whatwg.org/#create-writable-stream +export function createWritableStream( + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, +) { + $assert(typeof highWaterMark === "number" && !isNaN(highWaterMark) && highWaterMark >= 0); + + const internalStream = {}; + $initializeWritableStreamSlots(internalStream, {}); + const controller = new WritableStreamDefaultController(); + + $setUpWritableStreamDefaultController( + internalStream, + controller, + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, + ); + + return $createWritableStreamFromInternal(internalStream); +} + +export function createInternalWritableStreamFromUnderlyingSink(underlyingSink, strategy) { + const stream = {}; + + if (underlyingSink === undefined) underlyingSink = {}; + + if (strategy === undefined) strategy = {}; + + if (!$isObject(underlyingSink)) $throwTypeError("WritableStream constructor takes an object as first argument"); + + if ("type" in underlyingSink) $throwRangeError("Invalid type is specified"); + + const sizeAlgorithm = $extractSizeAlgorithm(strategy); + const highWaterMark = $extractHighWaterMark(strategy, 1); + + const underlyingSinkDict = {}; + if ("start" in underlyingSink) { + underlyingSinkDict["start"] = underlyingSink["start"]; + if (typeof underlyingSinkDict["start"] !== "function") $throwTypeError("underlyingSink.start should be a function"); + } + if ("write" in underlyingSink) { + underlyingSinkDict["write"] = underlyingSink["write"]; + if (typeof underlyingSinkDict["write"] !== "function") $throwTypeError("underlyingSink.write should be a function"); + } + if ("close" in underlyingSink) { + underlyingSinkDict["close"] = underlyingSink["close"]; + if (typeof underlyingSinkDict["close"] !== "function") $throwTypeError("underlyingSink.close should be a function"); + } + if ("abort" in underlyingSink) { + underlyingSinkDict["abort"] = underlyingSink["abort"]; + if (typeof underlyingSinkDict["abort"] !== "function") $throwTypeError("underlyingSink.abort should be a function"); + } + + $initializeWritableStreamSlots(stream, underlyingSink); + $setUpWritableStreamDefaultControllerFromUnderlyingSink( + stream, + underlyingSink, + underlyingSinkDict, + highWaterMark, + sizeAlgorithm, + ); + + return stream; +} + +export function initializeWritableStreamSlots(stream, underlyingSink) { + $putByIdDirectPrivate(stream, "state", "writable"); + $putByIdDirectPrivate(stream, "storedError", undefined); + $putByIdDirectPrivate(stream, "writer", undefined); + $putByIdDirectPrivate(stream, "controller", undefined); + $putByIdDirectPrivate(stream, "inFlightWriteRequest", undefined); + $putByIdDirectPrivate(stream, "closeRequest", undefined); + $putByIdDirectPrivate(stream, "inFlightCloseRequest", undefined); + $putByIdDirectPrivate(stream, "pendingAbortRequest", undefined); + $putByIdDirectPrivate(stream, "writeRequests", $createFIFO()); + $putByIdDirectPrivate(stream, "backpressure", false); + $putByIdDirectPrivate(stream, "underlyingSink", underlyingSink); +} + +export function writableStreamCloseForBindings(stream) { + if ($isWritableStreamLocked(stream)) + return Promise.$reject($makeTypeError("WritableStream.close method can only be used on non locked WritableStream")); + + if ($writableStreamCloseQueuedOrInFlight(stream)) + return Promise.$reject( + $makeTypeError("WritableStream.close method can only be used on a being close WritableStream"), + ); + + return $writableStreamClose(stream); +} + +export function writableStreamAbortForBindings(stream, reason) { + if ($isWritableStreamLocked(stream)) + return Promise.$reject($makeTypeError("WritableStream.abort method can only be used on non locked WritableStream")); + + return $writableStreamAbort(stream, reason); +} + +export function isWritableStreamLocked(stream) { + return $getByIdDirectPrivate(stream, "writer") !== undefined; +} + +export function setUpWritableStreamDefaultWriter(writer, stream) { + if ($isWritableStreamLocked(stream)) $throwTypeError("WritableStream is locked"); + + $putByIdDirectPrivate(writer, "stream", stream); + $putByIdDirectPrivate(stream, "writer", writer); + + const readyPromiseCapability = $newPromiseCapability(Promise); + const closedPromiseCapability = $newPromiseCapability(Promise); + $putByIdDirectPrivate(writer, "readyPromise", readyPromiseCapability); + $putByIdDirectPrivate(writer, "closedPromise", closedPromiseCapability); + + const state = $getByIdDirectPrivate(stream, "state"); + if (state === "writable") { + if ($writableStreamCloseQueuedOrInFlight(stream) || !$getByIdDirectPrivate(stream, "backpressure")) + readyPromiseCapability.$resolve.$call(); + } else if (state === "erroring") { + readyPromiseCapability.$reject.$call(undefined, $getByIdDirectPrivate(stream, "storedError")); + $markPromiseAsHandled(readyPromiseCapability.$promise); + } else if (state === "closed") { + readyPromiseCapability.$resolve.$call(); + closedPromiseCapability.$resolve.$call(); + } else { + $assert(state === "errored"); + const storedError = $getByIdDirectPrivate(stream, "storedError"); + readyPromiseCapability.$reject.$call(undefined, storedError); + $markPromiseAsHandled(readyPromiseCapability.$promise); + closedPromiseCapability.$reject.$call(undefined, storedError); + $markPromiseAsHandled(closedPromiseCapability.$promise); + } +} + +export function writableStreamAbort(stream, reason) { + const state = $getByIdDirectPrivate(stream, "state"); + if (state === "closed" || state === "errored") return Promise.$resolve(); + + const pendingAbortRequest = $getByIdDirectPrivate(stream, "pendingAbortRequest"); + if (pendingAbortRequest !== undefined) return pendingAbortRequest.promise.$promise; + + $assert(state === "writable" || state === "erroring"); + let wasAlreadyErroring = false; + if (state === "erroring") { + wasAlreadyErroring = true; + reason = undefined; + } + + const abortPromiseCapability = $newPromiseCapability(Promise); + $putByIdDirectPrivate(stream, "pendingAbortRequest", { + promise: abortPromiseCapability, + reason: reason, + wasAlreadyErroring: wasAlreadyErroring, + }); + + if (!wasAlreadyErroring) $writableStreamStartErroring(stream, reason); + return abortPromiseCapability.$promise; +} + +export function writableStreamClose(stream) { + const state = $getByIdDirectPrivate(stream, "state"); + if (state === "closed" || state === "errored") + return Promise.$reject($makeTypeError("Cannot close a writable stream that is closed or errored")); + + $assert(state === "writable" || state === "erroring"); + $assert(!$writableStreamCloseQueuedOrInFlight(stream)); + + const closePromiseCapability = $newPromiseCapability(Promise); + $putByIdDirectPrivate(stream, "closeRequest", closePromiseCapability); + + const writer = $getByIdDirectPrivate(stream, "writer"); + if (writer !== undefined && $getByIdDirectPrivate(stream, "backpressure") && state === "writable") + $getByIdDirectPrivate(writer, "readyPromise").$resolve.$call(); + + $writableStreamDefaultControllerClose($getByIdDirectPrivate(stream, "controller")); + + return closePromiseCapability.$promise; +} + +export function writableStreamAddWriteRequest(stream) { + $assert($isWritableStreamLocked(stream)); + $assert($getByIdDirectPrivate(stream, "state") === "writable"); + + const writePromiseCapability = $newPromiseCapability(Promise); + const writeRequests = $getByIdDirectPrivate(stream, "writeRequests"); + writeRequests.push(writePromiseCapability); + return writePromiseCapability.$promise; +} + +export function writableStreamCloseQueuedOrInFlight(stream) { + return ( + $getByIdDirectPrivate(stream, "closeRequest") !== undefined || + $getByIdDirectPrivate(stream, "inFlightCloseRequest") !== undefined + ); +} + +export function writableStreamDealWithRejection(stream, error) { + const state = $getByIdDirectPrivate(stream, "state"); + if (state === "writable") { + $writableStreamStartErroring(stream, error); + return; + } + + $assert(state === "erroring"); + $writableStreamFinishErroring(stream); +} + +export function writableStreamFinishErroring(stream) { + $assert($getByIdDirectPrivate(stream, "state") === "erroring"); + $assert(!$writableStreamHasOperationMarkedInFlight(stream)); + + $putByIdDirectPrivate(stream, "state", "errored"); + + const controller = $getByIdDirectPrivate(stream, "controller"); + $getByIdDirectPrivate(controller, "errorSteps").$call(); + + const storedError = $getByIdDirectPrivate(stream, "storedError"); + const requests = $getByIdDirectPrivate(stream, "writeRequests"); + for (var request = requests.shift(); request; request = requests.shift()) + request.$reject.$call(undefined, storedError); + + // TODO: is this still necessary? + $putByIdDirectPrivate(stream, "writeRequests", $createFIFO()); + + const abortRequest = $getByIdDirectPrivate(stream, "pendingAbortRequest"); + if (abortRequest === undefined) { + $writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + + $putByIdDirectPrivate(stream, "pendingAbortRequest", undefined); + if (abortRequest.wasAlreadyErroring) { + abortRequest.promise.$reject.$call(undefined, storedError); + $writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + + $getByIdDirectPrivate(controller, "abortSteps") + .$call(undefined, abortRequest.reason) + .$then( + () => { + abortRequest.promise.$resolve.$call(); + $writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + }, + reason => { + abortRequest.promise.$reject.$call(undefined, reason); + $writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + }, + ); +} + +export function writableStreamFinishInFlightClose(stream) { + const inFlightCloseRequest = $getByIdDirectPrivate(stream, "inFlightCloseRequest"); + inFlightCloseRequest.$resolve.$call(); + + $putByIdDirectPrivate(stream, "inFlightCloseRequest", undefined); + + const state = $getByIdDirectPrivate(stream, "state"); + $assert(state === "writable" || state === "erroring"); + + if (state === "erroring") { + $putByIdDirectPrivate(stream, "storedError", undefined); + const abortRequest = $getByIdDirectPrivate(stream, "pendingAbortRequest"); + if (abortRequest !== undefined) { + abortRequest.promise.$resolve.$call(); + $putByIdDirectPrivate(stream, "pendingAbortRequest", undefined); + } + } + + $putByIdDirectPrivate(stream, "state", "closed"); + + const writer = $getByIdDirectPrivate(stream, "writer"); + if (writer !== undefined) $getByIdDirectPrivate(writer, "closedPromise").$resolve.$call(); + + $assert($getByIdDirectPrivate(stream, "pendingAbortRequest") === undefined); + $assert($getByIdDirectPrivate(stream, "storedError") === undefined); +} + +export function writableStreamFinishInFlightCloseWithError(stream, error) { + const inFlightCloseRequest = $getByIdDirectPrivate(stream, "inFlightCloseRequest"); + $assert(inFlightCloseRequest !== undefined); + inFlightCloseRequest.$reject.$call(undefined, error); + + $putByIdDirectPrivate(stream, "inFlightCloseRequest", undefined); + + const state = $getByIdDirectPrivate(stream, "state"); + $assert(state === "writable" || state === "erroring"); + + const abortRequest = $getByIdDirectPrivate(stream, "pendingAbortRequest"); + if (abortRequest !== undefined) { + abortRequest.promise.$reject.$call(undefined, error); + $putByIdDirectPrivate(stream, "pendingAbortRequest", undefined); + } + + $writableStreamDealWithRejection(stream, error); +} + +export function writableStreamFinishInFlightWrite(stream) { + const inFlightWriteRequest = $getByIdDirectPrivate(stream, "inFlightWriteRequest"); + $assert(inFlightWriteRequest !== undefined); + inFlightWriteRequest.$resolve.$call(); + + $putByIdDirectPrivate(stream, "inFlightWriteRequest", undefined); +} + +export function writableStreamFinishInFlightWriteWithError(stream, error) { + const inFlightWriteRequest = $getByIdDirectPrivate(stream, "inFlightWriteRequest"); + $assert(inFlightWriteRequest !== undefined); + inFlightWriteRequest.$reject.$call(undefined, error); + + $putByIdDirectPrivate(stream, "inFlightWriteRequest", undefined); + + const state = $getByIdDirectPrivate(stream, "state"); + $assert(state === "writable" || state === "erroring"); + + $writableStreamDealWithRejection(stream, error); +} + +export function writableStreamHasOperationMarkedInFlight(stream) { + return ( + $getByIdDirectPrivate(stream, "inFlightWriteRequest") !== undefined || + $getByIdDirectPrivate(stream, "inFlightCloseRequest") !== undefined + ); +} + +export function writableStreamMarkCloseRequestInFlight(stream) { + const closeRequest = $getByIdDirectPrivate(stream, "closeRequest"); + $assert($getByIdDirectPrivate(stream, "inFlightCloseRequest") === undefined); + $assert(closeRequest !== undefined); + + $putByIdDirectPrivate(stream, "inFlightCloseRequest", closeRequest); + $putByIdDirectPrivate(stream, "closeRequest", undefined); +} + +export function writableStreamMarkFirstWriteRequestInFlight(stream) { + const writeRequests = $getByIdDirectPrivate(stream, "writeRequests"); + $assert($getByIdDirectPrivate(stream, "inFlightWriteRequest") === undefined); + $assert(writeRequests.isNotEmpty()); + + const writeRequest = writeRequests.shift(); + $putByIdDirectPrivate(stream, "inFlightWriteRequest", writeRequest); +} + +export function writableStreamRejectCloseAndClosedPromiseIfNeeded(stream) { + $assert($getByIdDirectPrivate(stream, "state") === "errored"); + + const storedError = $getByIdDirectPrivate(stream, "storedError"); + + const closeRequest = $getByIdDirectPrivate(stream, "closeRequest"); + if (closeRequest !== undefined) { + $assert($getByIdDirectPrivate(stream, "inFlightCloseRequest") === undefined); + closeRequest.$reject.$call(undefined, storedError); + $putByIdDirectPrivate(stream, "closeRequest", undefined); + } + + const writer = $getByIdDirectPrivate(stream, "writer"); + if (writer !== undefined) { + const closedPromise = $getByIdDirectPrivate(writer, "closedPromise"); + closedPromise.$reject.$call(undefined, storedError); + $markPromiseAsHandled(closedPromise.$promise); + } +} + +export function writableStreamStartErroring(stream, reason) { + $assert($getByIdDirectPrivate(stream, "storedError") === undefined); + $assert($getByIdDirectPrivate(stream, "state") === "writable"); + + const controller = $getByIdDirectPrivate(stream, "controller"); + $assert(controller !== undefined); + + $putByIdDirectPrivate(stream, "state", "erroring"); + $putByIdDirectPrivate(stream, "storedError", reason); + + const writer = $getByIdDirectPrivate(stream, "writer"); + if (writer !== undefined) $writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); + + if (!$writableStreamHasOperationMarkedInFlight(stream) && $getByIdDirectPrivate(controller, "started") === 1) + $writableStreamFinishErroring(stream); +} + +export function writableStreamUpdateBackpressure(stream, backpressure) { + $assert($getByIdDirectPrivate(stream, "state") === "writable"); + $assert(!$writableStreamCloseQueuedOrInFlight(stream)); + + const writer = $getByIdDirectPrivate(stream, "writer"); + if (writer !== undefined && backpressure !== $getByIdDirectPrivate(stream, "backpressure")) { + if (backpressure) $putByIdDirectPrivate(writer, "readyPromise", $newPromiseCapability(Promise)); + else $getByIdDirectPrivate(writer, "readyPromise").$resolve.$call(); + } + $putByIdDirectPrivate(stream, "backpressure", backpressure); +} + +export function writableStreamDefaultWriterAbort(writer, reason) { + const stream = $getByIdDirectPrivate(writer, "stream"); + $assert(stream !== undefined); + return $writableStreamAbort(stream, reason); +} + +export function writableStreamDefaultWriterClose(writer) { + const stream = $getByIdDirectPrivate(writer, "stream"); + $assert(stream !== undefined); + return $writableStreamClose(stream); +} + +export function writableStreamDefaultWriterCloseWithErrorPropagation(writer) { + const stream = $getByIdDirectPrivate(writer, "stream"); + $assert(stream !== undefined); + + const state = $getByIdDirectPrivate(stream, "state"); + + if ($writableStreamCloseQueuedOrInFlight(stream) || state === "closed") return Promise.$resolve(); + + if (state === "errored") return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); + + $assert(state === "writable" || state === "erroring"); + return $writableStreamDefaultWriterClose(writer); +} + +export function writableStreamDefaultWriterEnsureClosedPromiseRejected(writer, error) { + let closedPromiseCapability = $getByIdDirectPrivate(writer, "closedPromise"); + let closedPromise = closedPromiseCapability.$promise; + + if (($getPromiseInternalField(closedPromise, $promiseFieldFlags) & $promiseStateMask) !== $promiseStatePending) { + closedPromiseCapability = $newPromiseCapability(Promise); + closedPromise = closedPromiseCapability.$promise; + $putByIdDirectPrivate(writer, "closedPromise", closedPromiseCapability); + } + + closedPromiseCapability.$reject.$call(undefined, error); + $markPromiseAsHandled(closedPromise); +} + +export function writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) { + let readyPromiseCapability = $getByIdDirectPrivate(writer, "readyPromise"); + let readyPromise = readyPromiseCapability.$promise; + + if (($getPromiseInternalField(readyPromise, $promiseFieldFlags) & $promiseStateMask) !== $promiseStatePending) { + readyPromiseCapability = $newPromiseCapability(Promise); + readyPromise = readyPromiseCapability.$promise; + $putByIdDirectPrivate(writer, "readyPromise", readyPromiseCapability); + } + + readyPromiseCapability.$reject.$call(undefined, error); + $markPromiseAsHandled(readyPromise); +} + +export function writableStreamDefaultWriterGetDesiredSize(writer) { + const stream = $getByIdDirectPrivate(writer, "stream"); + $assert(stream !== undefined); + + const state = $getByIdDirectPrivate(stream, "state"); + + if (state === "errored" || state === "erroring") return null; + + if (state === "closed") return 0; + + return $writableStreamDefaultControllerGetDesiredSize($getByIdDirectPrivate(stream, "controller")); +} + +export function writableStreamDefaultWriterRelease(writer) { + const stream = $getByIdDirectPrivate(writer, "stream"); + $assert(stream !== undefined); + $assert($getByIdDirectPrivate(stream, "writer") === writer); + + const releasedError = $makeTypeError("writableStreamDefaultWriterRelease"); + + $writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError); + $writableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError); + + $putByIdDirectPrivate(stream, "writer", undefined); + $putByIdDirectPrivate(writer, "stream", undefined); +} + +export function writableStreamDefaultWriterWrite(writer, chunk) { + const stream = $getByIdDirectPrivate(writer, "stream"); + $assert(stream !== undefined); + + const controller = $getByIdDirectPrivate(stream, "controller"); + $assert(controller !== undefined); + const chunkSize = $writableStreamDefaultControllerGetChunkSize(controller, chunk); + + if (stream !== $getByIdDirectPrivate(writer, "stream")) + return Promise.$reject($makeTypeError("writer is not stream's writer")); + + const state = $getByIdDirectPrivate(stream, "state"); + if (state === "errored") return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); + + if ($writableStreamCloseQueuedOrInFlight(stream) || state === "closed") + return Promise.$reject($makeTypeError("stream is closing or closed")); + + if ($writableStreamCloseQueuedOrInFlight(stream) || state === "closed") + return Promise.$reject($makeTypeError("stream is closing or closed")); + + if (state === "erroring") return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); + + $assert(state === "writable"); + + const promise = $writableStreamAddWriteRequest(stream); + $writableStreamDefaultControllerWrite(controller, chunk, chunkSize); + return promise; +} + +export function setUpWritableStreamDefaultController( + stream, + controller, + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, +) { + $assert($isWritableStream(stream)); + $assert($getByIdDirectPrivate(stream, "controller") === undefined); + + $putByIdDirectPrivate(controller, "stream", stream); + $putByIdDirectPrivate(stream, "controller", controller); + + $resetQueue($getByIdDirectPrivate(controller, "queue")); + + $putByIdDirectPrivate(controller, "started", -1); + $putByIdDirectPrivate(controller, "startAlgorithm", startAlgorithm); + $putByIdDirectPrivate(controller, "strategySizeAlgorithm", sizeAlgorithm); + $putByIdDirectPrivate(controller, "strategyHWM", highWaterMark); + $putByIdDirectPrivate(controller, "writeAlgorithm", writeAlgorithm); + $putByIdDirectPrivate(controller, "closeAlgorithm", closeAlgorithm); + $putByIdDirectPrivate(controller, "abortAlgorithm", abortAlgorithm); + + const backpressure = $writableStreamDefaultControllerGetBackpressure(controller); + $writableStreamUpdateBackpressure(stream, backpressure); + + $writableStreamDefaultControllerStart(controller); +} + +export function writableStreamDefaultControllerStart(controller) { + if ($getByIdDirectPrivate(controller, "started") !== -1) return; + + $putByIdDirectPrivate(controller, "started", 0); + + const startAlgorithm = $getByIdDirectPrivate(controller, "startAlgorithm"); + $putByIdDirectPrivate(controller, "startAlgorithm", undefined); + const stream = $getByIdDirectPrivate(controller, "stream"); + return Promise.$resolve(startAlgorithm.$call()).$then( + () => { + const state = $getByIdDirectPrivate(stream, "state"); + $assert(state === "writable" || state === "erroring"); + $putByIdDirectPrivate(controller, "started", 1); + $writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + }, + error => { + const state = $getByIdDirectPrivate(stream, "state"); + $assert(state === "writable" || state === "erroring"); + $putByIdDirectPrivate(controller, "started", 1); + $writableStreamDealWithRejection(stream, error); + }, + ); +} + +export function setUpWritableStreamDefaultControllerFromUnderlyingSink( + stream, + underlyingSink, + underlyingSinkDict, + highWaterMark, + sizeAlgorithm, +) { + const controller = new $WritableStreamDefaultController(); + + let startAlgorithm = () => {}; + let writeAlgorithm = () => { + return Promise.$resolve(); + }; + let closeAlgorithm = () => { + return Promise.$resolve(); + }; + let abortAlgorithm = () => { + return Promise.$resolve(); + }; + + if ("start" in underlyingSinkDict) { + const startMethod = underlyingSinkDict["start"]; + startAlgorithm = () => $promiseInvokeOrNoopMethodNoCatch(underlyingSink, startMethod, [controller]); + } + if ("write" in underlyingSinkDict) { + const writeMethod = underlyingSinkDict["write"]; + writeAlgorithm = chunk => $promiseInvokeOrNoopMethod(underlyingSink, writeMethod, [chunk, controller]); + } + if ("close" in underlyingSinkDict) { + const closeMethod = underlyingSinkDict["close"]; + closeAlgorithm = () => $promiseInvokeOrNoopMethod(underlyingSink, closeMethod, []); + } + if ("abort" in underlyingSinkDict) { + const abortMethod = underlyingSinkDict["abort"]; + abortAlgorithm = reason => $promiseInvokeOrNoopMethod(underlyingSink, abortMethod, [reason]); + } + + $setUpWritableStreamDefaultController( + stream, + controller, + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, + ); +} + +export function writableStreamDefaultControllerAdvanceQueueIfNeeded(controller) { + const stream = $getByIdDirectPrivate(controller, "stream"); + + if ($getByIdDirectPrivate(controller, "started") !== 1) return; + + $assert(stream !== undefined); + if ($getByIdDirectPrivate(stream, "inFlightWriteRequest") !== undefined) return; + + const state = $getByIdDirectPrivate(stream, "state"); + $assert(state !== "closed" || state !== "errored"); + if (state === "erroring") { + $writableStreamFinishErroring(stream); + return; + } + + const queue = $getByIdDirectPrivate(controller, "queue"); + + if (queue.content?.isEmpty() ?? false) return; + + const value = $peekQueueValue(queue); + if (value === $isCloseSentinel) $writableStreamDefaultControllerProcessClose(controller); + else $writableStreamDefaultControllerProcessWrite(controller, value); +} + +export function isCloseSentinel() {} + +export function writableStreamDefaultControllerClearAlgorithms(controller) { + $putByIdDirectPrivate(controller, "writeAlgorithm", undefined); + $putByIdDirectPrivate(controller, "closeAlgorithm", undefined); + $putByIdDirectPrivate(controller, "abortAlgorithm", undefined); + $putByIdDirectPrivate(controller, "strategySizeAlgorithm", undefined); +} + +export function writableStreamDefaultControllerClose(controller) { + $enqueueValueWithSize($getByIdDirectPrivate(controller, "queue"), $isCloseSentinel, 0); + $writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); +} + +export function writableStreamDefaultControllerError(controller, error) { + const stream = $getByIdDirectPrivate(controller, "stream"); + $assert(stream !== undefined); + $assert($getByIdDirectPrivate(stream, "state") === "writable"); + + $writableStreamDefaultControllerClearAlgorithms(controller); + $writableStreamStartErroring(stream, error); +} + +export function writableStreamDefaultControllerErrorIfNeeded(controller, error) { + const stream = $getByIdDirectPrivate(controller, "stream"); + if ($getByIdDirectPrivate(stream, "state") === "writable") $writableStreamDefaultControllerError(controller, error); +} + +export function writableStreamDefaultControllerGetBackpressure(controller) { + const desiredSize = $writableStreamDefaultControllerGetDesiredSize(controller); + return desiredSize <= 0; +} + +export function writableStreamDefaultControllerGetChunkSize(controller, chunk) { + try { + return $getByIdDirectPrivate(controller, "strategySizeAlgorithm").$call(undefined, chunk); + } catch (e) { + $writableStreamDefaultControllerErrorIfNeeded(controller, e); + return 1; + } +} + +export function writableStreamDefaultControllerGetDesiredSize(controller) { + return $getByIdDirectPrivate(controller, "strategyHWM") - $getByIdDirectPrivate(controller, "queue").size; +} + +export function writableStreamDefaultControllerProcessClose(controller) { + const stream = $getByIdDirectPrivate(controller, "stream"); + + $writableStreamMarkCloseRequestInFlight(stream); + $dequeueValue($getByIdDirectPrivate(controller, "queue")); + + $assert($getByIdDirectPrivate(controller, "queue").content?.isEmpty()); + + const sinkClosePromise = $getByIdDirectPrivate(controller, "closeAlgorithm").$call(); + $writableStreamDefaultControllerClearAlgorithms(controller); + + sinkClosePromise.$then( + () => { + $writableStreamFinishInFlightClose(stream); + }, + reason => { + $writableStreamFinishInFlightCloseWithError(stream, reason); + }, + ); +} + +export function writableStreamDefaultControllerProcessWrite(controller, chunk) { + const stream = $getByIdDirectPrivate(controller, "stream"); + + $writableStreamMarkFirstWriteRequestInFlight(stream); + + const sinkWritePromise = $getByIdDirectPrivate(controller, "writeAlgorithm").$call(undefined, chunk); + + sinkWritePromise.$then( + () => { + $writableStreamFinishInFlightWrite(stream); + const state = $getByIdDirectPrivate(stream, "state"); + $assert(state === "writable" || state === "erroring"); + + $dequeueValue($getByIdDirectPrivate(controller, "queue")); + if (!$writableStreamCloseQueuedOrInFlight(stream) && state === "writable") { + const backpressure = $writableStreamDefaultControllerGetBackpressure(controller); + $writableStreamUpdateBackpressure(stream, backpressure); + } + $writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + }, + reason => { + const state = $getByIdDirectPrivate(stream, "state"); + if (state === "writable") $writableStreamDefaultControllerClearAlgorithms(controller); + + $writableStreamFinishInFlightWriteWithError(stream, reason); + }, + ); +} + +export function writableStreamDefaultControllerWrite(controller, chunk, chunkSize) { + try { + $enqueueValueWithSize($getByIdDirectPrivate(controller, "queue"), chunk, chunkSize); + + const stream = $getByIdDirectPrivate(controller, "stream"); + + const state = $getByIdDirectPrivate(stream, "state"); + if (!$writableStreamCloseQueuedOrInFlight(stream) && state === "writable") { + const backpressure = $writableStreamDefaultControllerGetBackpressure(controller); + $writableStreamUpdateBackpressure(stream, backpressure); + } + $writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + } catch (e) { + $writableStreamDefaultControllerErrorIfNeeded(controller, e); + } +} diff --git a/src/js/builtins/builtins.d.ts b/src/js/builtins/builtins.d.ts new file mode 100644 index 000000000..d2e7037dc --- /dev/null +++ b/src/js/builtins/builtins.d.ts @@ -0,0 +1,492 @@ +// Typedefs for JSC intrinsics. Instead of @, we use $ +type TODO = any; + +/** Place this directly above a function declaration (like a decorator) to make it a getter. */ +declare const $getter: never; +/** Assign to this directly above a function declaration (like a decorator) to override the function's display name. */ +declare var $overriddenName: string; +/** ??? */ +declare var $linkTimeConstant: never; +/** Assign to this directly above a function declaration (like a decorator) to set visibility */ +declare var $visibility: "Public" | "Private"; +/** ??? */ +declare var $nakedConstructor: never; +/** Assign to this directly above a function declaration (like a decorator) to set intrinsic */ +declare var $intrinsic: string; +declare var $constructor; +/** Place this directly above a function declaration (like a decorator) to NOT include "use strict" */ +declare var $sloppy; + +declare function $extractHighWaterMarkFromQueuingStrategyInit(obj: any): any; + +// JSC defines their intrinsics in a nice list here: +// https://github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/bytecode/BytecodeIntrinsicRegistry.h +// +// And implemented here: (search for "emit_intrinsic_<name>", like "emit_intrinsic_arrayPush") +// https://github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp + +/** Assert a value is true */ +declare function $assert(index: any): void; +/** returns `arguments[index]` */ +declare function $argument<T = any>(index: number): any; +/** returns number of arguments */ +declare function $argumentCount(): number; +/** array.push(item) */ +declare function $arrayPush(array: T[], item: T): void; +/** gets a property on an object */ +declare function $getByIdDirect<T = any>(obj: any, key: string): T; +/** + * gets a private property on an object. translates to the `op_get_by_id_direct` bytecode. + * + * TODO: clarify what private means exactly. + */ +declare function $getByIdDirectPrivate<T = any>(obj: any, key: string): T; +/** + * gets a property on an object + */ +declare function $getByValWithThis(target: any, receiver: any, propertyKey: string): void; +/** gets the prototype of an object */ +declare function $getPrototypeOf(value: any): any; +/** gets an internal property on a promise + * + * You can pass + * - $promiseFieldFlags - get a number with flags + * - $promiseFieldReactionsOrResult - get the result (like Bun.peek) + */ +declare function $getPromiseInternalField<K extends PromiseFieldType, V>( + promise: Promise<V>, + key: K, +): PromiseFieldToValue<K, V>; +declare function $getGeneratorInternalField(): TODO; +declare function $getAsyncGeneratorInternalField(): TODO; +declare function $getAbstractModuleRecordInternalField(): TODO; +declare function $getArrayIteratorInternalField(): TODO; +declare function $getStringIteratorInternalField(): TODO; +declare function $getMapIteratorInternalField(): TODO; +declare function $getSetIteratorInternalField(): TODO; +declare function $getProxyInternalField(): TODO; +declare function $idWithProfile(): TODO; +declare function $isObject(obj: unknown): obj is object; +declare function $isCallable(fn: unknown): fn is CallableFunction; +declare function $isConstructor(fn: unknown): fn is { new (...args: any[]): any }; +declare function $isJSArray(obj: unknown): obj is any[]; +declare function $isProxyObject(obj: unknown): obj is Proxy; +declare function $isDerivedArray(): TODO; +declare function $isGenerator(obj: unknown): obj is Generator<any, any, any>; +declare function $isAsyncGenerator(obj: unknown): obj is AsyncGenerator<any, any, any>; +declare function $isPromise(obj: unknown): obj is Promise<any>; +declare function $isRegExpObject(obj: unknown): obj is RegExp; +declare function $isMap<K, V>(obj: unknown): obj is Map<K, V>; +declare function $isSet<V>(obj: unknown): obj is Set<V>; +declare function $isShadowRealm(obj: unknown): obj is ShadowRealm; +declare function $isStringIterator(obj: unknown): obj is Iterator<string>; +declare function $isArrayIterator(obj: unknown): obj is Iterator<any>; +declare function $isMapIterator(obj: unknown): obj is Iterator<any>; +declare function $isSetIterator(obj: unknown): obj is Iterator<any>; +declare function $isUndefinedOrNull(obj: unknown): obj is null | undefined; +declare function $tailCallForwardArguments(): TODO; +/** + * **NOTE** - use `throw new TypeError()` instead. it compiles to the same builtin + * @deprecated + */ +declare function $throwTypeError(message: string): never; +/** + * **NOTE** - use `throw new RangeError()` instead. it compiles to the same builtin + * @deprecated + */ +declare function $throwRangeError(message: string): never; +/** + * **NOTE** - use `throw new OutOfMemoryError()` instead. it compiles to the same builtin + * @deprecated + */ +declare function $throwOutOfMemoryError(): never; +declare function $tryGetById(): TODO; +declare function $tryGetByIdWithWellKnownSymbol(obj: any, key: WellKnownSymbol): any; +declare function $putByIdDirect(obj: any, key: PropertyKey, value: any): void; +declare function $putByIdDirectPrivate(obj: any, key: PropertyKey, value: any): void; +declare function $putByValDirect(obj: any, key: PropertyKey, value: any): void; +declare function $putByValWithThisSloppy(): TODO; +declare function $putByValWithThisStrict(): TODO; +declare function $putPromiseInternalField<T extends PromiseFieldType, P extends Promise<any>>( + promise: P, + key: T, + value: PromiseFieldToValue<T, P>, +): void; +declare function $putGeneratorInternalField(): TODO; +declare function $putAsyncGeneratorInternalField(): TODO; +declare function $putArrayIteratorInternalField(): TODO; +declare function $putStringIteratorInternalField(): TODO; +declare function $putMapIteratorInternalField(): TODO; +declare function $putSetIteratorInternalField(): TODO; +declare function $superSamplerBegin(): TODO; +declare function $superSamplerEnd(): TODO; +declare function $toNumber(x: any): number; +declare function $toString(x: any): string; +declare function $toPropertyKey(x: any): PropertyKey; +/** + * Often used like + * `$toObject(this, "Class.prototype.method requires that |this| not be null or undefined");` + */ +declare function $toObject(object: any, errorMessage?: string): object; +declare function $newArrayWithSize<T>(size: number): T[]; +declare function $newArrayWithSpecies(): TODO; +declare function $newPromise(): TODO; +declare function $createPromise(): TODO; +declare const $iterationKindKey: TODO; +declare const $iterationKindValue: TODO; +declare const $iterationKindEntries: TODO; +declare const $MAX_ARRAY_INDEX: number; +declare const $MAX_STRING_LENGTH: number; +declare const $MAX_SAFE_INTEGER: number; +declare const $ModuleFetch: number; +declare const $ModuleTranslate: number; +declare const $ModuleInstantiate: number; +declare const $ModuleSatisfy: number; +declare const $ModuleLink: number; +declare const $ModuleReady: number; +declare const $promiseRejectionReject: TODO; +declare const $promiseRejectionHandle: TODO; +declare const $promiseStatePending: number; +declare const $promiseStateFulfilled: number; +declare const $promiseStateRejected: number; +declare const $promiseStateMask: number; +declare const $promiseFlagsIsHandled: number; +declare const $promiseFlagsIsFirstResolvingFunctionCalled: number; +declare const $promiseFieldFlags: unique symbol; +declare const $promiseFieldReactionsOrResult: unique symbol; +declare const $proxyFieldTarget: TODO; +declare const $proxyFieldHandler: TODO; +declare const $generatorFieldState: TODO; +declare const $generatorFieldNext: TODO; +declare const $generatorFieldThis: TODO; +declare const $generatorFieldFrame: TODO; +declare const $generatorFieldContext: TODO; +declare const $GeneratorResumeModeNormal: TODO; +declare const $GeneratorResumeModeThrow: TODO; +declare const $GeneratorResumeModeReturn: TODO; +declare const $GeneratorStateCompleted: TODO; +declare const $GeneratorStateExecuting: TODO; +declare const $arrayIteratorFieldIndex: TODO; +declare const $arrayIteratorFieldIteratedObject: TODO; +declare const $arrayIteratorFieldKind: TODO; +declare const $mapIteratorFieldMapBucket: TODO; +declare const $mapIteratorFieldKind: TODO; +declare const $setIteratorFieldSetBucket: TODO; +declare const $setIteratorFieldKind: TODO; +declare const $stringIteratorFieldIndex: TODO; +declare const $stringIteratorFieldIteratedString: TODO; +declare const $asyncGeneratorFieldSuspendReason: TODO; +declare const $asyncGeneratorFieldQueueFirst: TODO; +declare const $asyncGeneratorFieldQueueLast: TODO; +declare const $AsyncGeneratorStateCompleted: TODO; +declare const $AsyncGeneratorStateExecuting: TODO; +declare const $AsyncGeneratorStateAwaitingReturn: TODO; +declare const $AsyncGeneratorStateSuspendedStart: TODO; +declare const $AsyncGeneratorStateSuspendedYield: TODO; +declare const $AsyncGeneratorSuspendReasonYield: TODO; +declare const $AsyncGeneratorSuspendReasonAwait: TODO; +declare const $AsyncGeneratorSuspendReasonNone: TODO; +declare const $abstractModuleRecordFieldState: TODO; + +// We define our intrinsics in ./BunBuiltinNames.h. Some of those are globals. + +declare var $_events: TODO; +declare function $abortAlgorithm(): TODO; +declare function $abortSteps(): TODO; +declare function $addEventListener(): TODO; +declare function $appendFromJS(): TODO; +declare function $argv(): TODO; +declare function $assignToStream(): TODO; +declare function $associatedReadableByteStreamController(): TODO; +declare function $autoAllocateChunkSize(): TODO; +declare function $backpressure(): TODO; +declare function $backpressureChangePromise(): TODO; +declare function $basename(): TODO; +declare function $body(): TODO; +declare function $bunNativePtr(): TODO; +declare function $bunNativeType(): TODO; +declare function $byobRequest(): TODO; +declare function $cancel(): TODO; +declare function $cancelAlgorithm(): TODO; +declare function $chdir(): TODO; +declare function $cloneArrayBuffer(a, b, c): TODO; +declare function $close(): TODO; +declare function $closeAlgorithm(): TODO; +declare function $closeRequest(): TODO; +declare function $closeRequested(): TODO; +declare function $closed(): TODO; +declare function $closedPromise(): TODO; +declare function $closedPromiseCapability(): TODO; +declare function $code(): TODO; +declare const $commonJSSymbol: unique symbol; +declare function $connect(): TODO; +declare function $consumeReadableStream(): TODO; +declare function $controlledReadableStream(): TODO; +declare function $controller(): TODO; +declare function $cork(): TODO; +declare function $createEmptyReadableStream(): TODO; +declare function $createFIFO(): TODO; +declare function $createNativeReadableStream(): TODO; +declare function $createReadableStream(): TODO; +declare function $createUninitializedArrayBuffer(size: number): ArrayBuffer; +declare function $createWritableStreamFromInternal(): TODO; +declare function $cwd(): TODO; +declare function $data(): TODO; +declare function $dataView(): TODO; +declare function $decode(): TODO; +declare function $delimiter(): TODO; +declare function $destroy(): TODO; +declare function $dir(): TODO; +declare function $direct(): TODO; +declare function $dirname(): TODO; +declare function $disturbed(): TODO; +declare function $document(): TODO; +declare function $encode(): TODO; +declare function $encoding(): TODO; +declare function $end(): TODO; +declare function $errno(): TODO; +declare function $errorSteps(): TODO; +declare function $execArgv(): TODO; +declare function $extname(): TODO; +declare function $failureKind(): TODO; +declare function $fatal(): TODO; +declare function $fetch(): TODO; +declare function $fetchRequest(): TODO; +declare function $file(): TODO; +declare function $filePath(): TODO; +declare function $fillFromJS(): TODO; +declare function $filter(): TODO; +declare function $finishConsumingStream(): TODO; +declare function $flush(): TODO; +declare function $flushAlgorithm(): TODO; +declare function $format(): TODO; +declare function $fulfillModuleSync(key: string): void; +declare function $get(): TODO; +declare function $getInternalWritableStream(writable: WritableStream): TODO; +declare function $handleEvent(): TODO; +declare function $hash(): TODO; +declare function $header(): TODO; +declare function $headers(): TODO; +declare function $highWaterMark(): TODO; +declare function $host(): TODO; +declare function $hostname(): TODO; +declare function $href(): TODO; +declare function $ignoreBOM(): TODO; +declare function $importer(): TODO; +declare function $inFlightCloseRequest(): TODO; +declare function $inFlightWriteRequest(): TODO; +declare function $initializeWith(): TODO; +declare function $internalRequire(path: string): TODO; +declare function $internalStream(): TODO; +declare function $internalWritable(): TODO; +declare function $isAbortSignal(signal: unknown): signal is AbortSignal; +declare function $isAbsolute(): TODO; +declare function $isDisturbed(): TODO; +declare function $isPaused(): TODO; +declare function $isWindows(): TODO; +declare function $join(): TODO; +declare function $kind(): TODO; +declare function $lazy(): TODO; +declare function $lazyLoad(): TODO; +declare function $lazyStreamPrototypeMap(): TODO; +declare function $loadModule(): TODO; +declare function $localStreams(): TODO; +declare function $main(): TODO; +declare function $makeDOMException(): TODO; +declare function $makeGetterTypeError(className: string, prop: string): Error; +declare function $makeThisTypeError(className: string, method: string): Error; +declare function $map(): TODO; +declare function $method(): TODO; +declare function $nextTick(): TODO; +declare function $normalize(): TODO; +declare function $on(): TODO; +declare function $once(): TODO; +declare function $options(): TODO; +declare function $origin(): TODO; +declare function $ownerReadableStream(): TODO; +declare function $parse(): TODO; +declare function $password(): TODO; +declare function $patch(): TODO; +declare function $path(): TODO; +declare function $pathname(): TODO; +declare function $pause(): TODO; +declare function $pendingAbortRequest(): TODO; +declare function $pendingPullIntos(): TODO; +declare function $pid(): TODO; +declare function $pipe(): TODO; +declare function $port(): TODO; +declare function $post(): TODO; +declare function $ppid(): TODO; +declare function $prependEventListener(): TODO; +declare function $process(): TODO; +declare function $protocol(): TODO; +declare function $pull(): TODO; +declare function $pullAgain(): TODO; +declare function $pullAlgorithm(): TODO; +declare function $pulling(): TODO; +declare function $put(): TODO; +declare function $queue(): TODO; +declare function $read(): TODO; +declare function $readIntoRequests(): TODO; +declare function $readRequests(): TODO; +declare function $readable(): TODO; +declare function $readableStreamController(): TODO; +declare function $readableStreamToArray(): TODO; +declare function $reader(): TODO; +declare function $readyPromise(): TODO; +declare function $readyPromiseCapability(): TODO; +declare function $redirect(): TODO; +declare function $relative(): TODO; +declare function $releaseLock(): TODO; +declare function $removeEventListener(): TODO; +declare function $require(): TODO; +declare function $requireESM(path: string): any; +declare const $requireMap: Map<string, TODO>; +declare function $resolve(name: string, from: string): Promise<string>; +declare function $resolveSync(name: string, from: string): string; +declare function $resume(): TODO; +declare function $search(): TODO; +declare function $searchParams(): TODO; +declare function $self(): TODO; +declare function $sep(): TODO; +declare function $setBody(): TODO; +declare function $setStatus(): TODO; +declare function $setup(): TODO; +declare function $sink(): TODO; +declare function $size(): TODO; +declare function $start(): TODO; +declare function $startAlgorithm(): TODO; +declare function $startConsumingStream(): TODO; +declare function $startDirectStream(): TODO; +declare function $started(): TODO; +declare function $startedPromise(): TODO; +declare function $state(): TODO; +declare function $status(): TODO; +declare function $storedError(): TODO; +declare function $strategy(): TODO; +declare function $strategyHWM(): TODO; +declare function $strategySizeAlgorithm(): TODO; +declare function $stream(): TODO; +declare function $streamClosed(): TODO; +declare function $streamClosing(): TODO; +declare function $streamErrored(): TODO; +declare function $streamReadable(): TODO; +declare function $streamWaiting(): TODO; +declare function $streamWritable(): TODO; +declare function $structuredCloneForStream(): TODO; +declare function $syscall(): TODO; +declare function $textDecoderStreamDecoder(): TODO; +declare function $textDecoderStreamTransform(): TODO; +declare function $textEncoderStreamEncoder(): TODO; +declare function $textEncoderStreamTransform(): TODO; +declare function $toNamespacedPath(): TODO; +declare function $trace(): TODO; +declare function $transformAlgorithm(): TODO; +declare function $uncork(): TODO; +declare function $underlyingByteSource(): TODO; +declare function $underlyingSink(): TODO; +declare function $underlyingSource(): TODO; +declare function $unpipe(): TODO; +declare function $unshift(): TODO; +declare function $url(): TODO; +declare function $username(): TODO; +declare function $version(): TODO; +declare function $versions(): TODO; +declare function $view(): TODO; +declare function $whenSignalAborted(signal: AbortSignal, cb: (reason: any) => void): TODO; +declare function $writable(): TODO; +declare function $write(): TODO; +declare function $writeAlgorithm(): TODO; +declare function $writeRequests(): TODO; +declare function $writer(): TODO; +declare function $writing(): TODO; +declare function $written(): TODO; + +// The following I cannot find any definitions of, but they are functional. +declare function $toLength(length: number): number; +declare function $isTypedArrayView(obj: unknown): obj is ArrayBufferView | DataView | Uint8Array; +declare function $setStateToMax(target: any, state: number): void; +declare function $trunc(target: number): number; +declare function $newPromiseCapability(C: PromiseConstructor): TODO; +/** @deprecated, use new TypeError instead */ +declare function $makeTypeError(message: string): TypeError; +declare function $newHandledRejectedPromise(error: unknown): Promise<never>; + +// Types used in the above functions +type PromiseFieldType = typeof $promiseFieldFlags | typeof $promiseFieldReactionsOrResult; +type PromiseFieldToValue<X extends PromiseFieldType, V> = X extends typeof $promiseFieldFlags + ? number + : X extends typeof $promiseFieldReactionsOrResult + ? V | any + : any; +type WellKnownSymbol = keyof { [K in keyof SymbolConstructor as SymbolConstructor[K] extends symbol ? K : never]: K }; + +// You can also `@` on any method on a classes to avoid prototype pollution and secret internals +type ClassWithIntrinsics<T> = { [K in keyof T as T[K] extends Function ? `$${K}` : never]: T[K] }; + +declare interface Map<K, V> extends ClassWithIntrinsics<Map<K, V>> {} +declare interface CallableFunction extends ClassWithIntrinsics<CallableFunction> {} +declare interface Promise<T> extends ClassWithIntrinsics<Promise<T>> {} +declare interface ArrayBufferConstructor<T> extends ClassWithIntrinsics<ArrayBufferConstructor<T>> {} +declare interface PromiseConstructor<T> extends ClassWithIntrinsics<PromiseConstructor<T>> {} + +declare interface UnderlyingSource { + $lazy: boolean; + $bunNativeType: number; + $bunNativePtr: number; + autoAllocateChunkSize?: number; +} + +declare class OutOfMemoryError { + constructor(); +} + +declare class ReadableStreamDefaultController { + constructor( + stream: unknown, + underlyingSource: unknown, + size: unknown, + highWaterMark: unknown, + $isReadableStream: typeof $isReadableStream, + ); +} +declare class ReadableByteStreamController { + constructor( + stream: unknown, + underlyingSource: unknown, + strategy: unknown, + $isReadableStream: typeof $isReadableStream, + ); +} +declare class ReadableStreamBYOBRequest { + constructor(stream: unknown, view: unknown, $isReadableStream: typeof $isReadableStream); +} +declare class ReadableStreamBYOBReader { + constructor(stream: unknown); +} + +// Inlining our enum types +declare const $ImportKindIdToLabel: Array<import("bun").ImportKind>; +declare const $ImportKindLabelToId: Record<import("bun").ImportKind, number>; +declare const $LoaderIdToLabel: Array<import("bun").Loader>; +declare const $LoaderLabelToId: Record<import("bun").Loader, number>; + +// not a builtin, but a build-time macro of our own +/** Returns a not implemented error that points to a github issue. */ +declare function notImplementedIssue(issueNumber: number, description: string): Error; +/** Return a function that throws a not implemented error that points to a github issue */ +declare function notImplementedIssueFn(issueNumber: number, description: string): (...args: any[]) => never; + +declare type JSCSourceCodeObject = unique symbol; + +declare interface Function { + path: string; +} + +declare var $Buffer: { + new (a: any, b?: any, c?: any): Buffer; +}; + +declare interface Error { + code?: string; +} diff --git a/src/js/builtins/codegen/builtin-parser.ts b/src/js/builtins/codegen/builtin-parser.ts new file mode 100644 index 000000000..e96d79c63 --- /dev/null +++ b/src/js/builtins/codegen/builtin-parser.ts @@ -0,0 +1,89 @@ +import { applyReplacements } from "./replacements"; + +/** + * Slices a string until it hits a }, but keeping in mind JS comments, + * regex, template literals, comments, and matching { + * + * Used to extract function bodies without parsing the code. + * + * If you pass replace=true, it will run replacements on the code + */ +export function sliceSourceCode( + contents: string, + replace: boolean, +): { result: string; rest: string; usesThis: boolean } { + let bracketCount = 0; + let i = 0; + let result = ""; + let usesThis = false; + while (contents.length) { + // TODO: template literal, regexp + // these are important because our replacement logic would replace intrinsics + // within these, when it should remain as the literal dollar. + // but this isn't used in the codebase + i = contents.match(/\/\*|\/\/|'|"|{|}|`/)?.index ?? contents.length; + const chunk = replace ? applyReplacements(contents.slice(0, i)) : contents.slice(0, i); + if (chunk.includes("this")) usesThis = true; + result += chunk; + contents = contents.slice(i); + if (!contents.length) break; + if (contents.startsWith("/*")) { + i = contents.slice(2).indexOf("*/") + 2; + } else if (contents.startsWith("//")) { + i = contents.slice(2).indexOf("\n") + 2; + } else if (contents.startsWith("'")) { + i = contents.slice(1).match(/(?<!\\)'/)!.index! + 2; + } else if (contents.startsWith('"')) { + i = contents.slice(1).match(/(?<!\\)"/)!.index! + 2; + } else if (contents.startsWith("`")) { + const { result: result2, rest } = sliceTemplateLiteralSourceCode(contents.slice(1), replace); + result += "`" + result2; + contents = rest; + continue; + } else if (contents.startsWith("{")) { + bracketCount++; + i = 1; + } else if (contents.startsWith("}")) { + bracketCount--; + if (bracketCount <= 0) { + result += "}"; + contents = contents.slice(1); + break; + } + i = 1; + } else { + throw new Error("TODO"); + } + result += contents.slice(0, i); + contents = contents.slice(i); + } + + return { result, rest: contents, usesThis }; +} + +function sliceTemplateLiteralSourceCode(contents: string, replace: boolean) { + let i = 0; + let result = ""; + let usesThis = false; + while (contents.length) { + i = contents.match(/`|\${/)!.index!; + result += contents.slice(0, i); + contents = contents.slice(i); + if (!contents.length) break; + if (contents.startsWith("`")) { + result += "`"; + contents = contents.slice(1); + break; + } else if (contents.startsWith("$")) { + const { result: result2, rest, usesThis: usesThisVal } = sliceSourceCode(contents.slice(1), replace); + result += "$" + result2; + contents = rest; + usesThis ||= usesThisVal; + continue; + } else { + throw new Error("TODO"); + } + } + + return { result, rest: contents, usesThis }; +} diff --git a/src/js/builtins/codegen/helpers.ts b/src/js/builtins/codegen/helpers.ts new file mode 100644 index 000000000..6345f8ffa --- /dev/null +++ b/src/js/builtins/codegen/helpers.ts @@ -0,0 +1,25 @@ +export function fmtCPPString(str: string) { + return ( + '"' + + str + .replace(/\\/g, "\\\\") + .replace(/"/g, '\\"') + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/\t/g, "\\t") + .replace(/\?/g, "\\?") + // https://stackoverflow.com/questions/1234582 + '"' + ); +} + +export function cap(str: string) { + return str[0].toUpperCase() + str.slice(1); +} + +export function low(str: string) { + if (str.startsWith("JS")) { + return "js" + str.slice(2); + } + + return str[0].toLowerCase() + str.slice(1); +} diff --git a/src/js/builtins/codegen/index.ts b/src/js/builtins/codegen/index.ts new file mode 100644 index 000000000..e20601a15 --- /dev/null +++ b/src/js/builtins/codegen/index.ts @@ -0,0 +1,627 @@ +import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "fs"; +import path from "path"; +import { sliceSourceCode } from "./builtin-parser"; +import { applyGlobalReplacements, enums, globalsToPrefix } from "./replacements"; +import { cap, fmtCPPString, low } from "./helpers"; + +console.log("Bundling Bun builtins..."); + +const MINIFY = process.argv.includes("--minify") || process.argv.includes("-m"); +const PARALLEL = process.argv.includes("--parallel") || process.argv.includes("-p"); +const KEEP_TMP = process.argv.includes("--keep-tmp") || process.argv.includes("-k"); + +const SRC_DIR = path.join(import.meta.dir, "../"); +const OUT_DIR = path.join(SRC_DIR, "../out"); +const TMP_DIR = path.join(SRC_DIR, "../out/tmp"); + +if (existsSync(TMP_DIR)) rmSync(TMP_DIR, { recursive: true }); +mkdirSync(TMP_DIR); + +const define = { + "process.env.NODE_ENV": "development", + "process.platform": process.platform, + "process.arch": process.arch, +}; + +for (const name in enums) { + const value = enums[name]; + if (typeof value !== "object") throw new Error("Invalid enum object " + name + " defined in " + import.meta.file); + if (typeof value === null) throw new Error("Invalid enum object " + name + " defined in " + import.meta.file); + const keys = Array.isArray(value) ? value : Object.keys(value).filter(k => !k.match(/^[0-9]+$/)); + define[`__intrinsic__${name}IdToLabel`] = "[" + keys.map(k => `"${k}"`).join(", ") + "]"; + define[`__intrinsic__${name}LabelToId`] = "{" + keys.map(k => `"${k}": ${keys.indexOf(k)}`).join(", ") + "}"; +} + +for (const name of globalsToPrefix) { + define[name] = "__intrinsic__" + name; +} + +interface ParsedBuiltin { + name: string; + params: string[]; + directives: Record<string, any>; + source: string; + async: boolean; +} +interface BundledBuiltin { + name: string; + directives: Record<string, any>; + isGetter: boolean; + isConstructor: boolean; + isLinkTimeConstant: boolean; + isNakedConstructor: boolean; + intrinsic: string; + overriddenName: string; + source: string; + params: string[]; + visibility: string; +} + +/** + * Source .ts file --> Array<bundled js function code> + */ +async function processFileSplit(filename: string): Promise<{ functions: BundledBuiltin[]; internal: boolean }> { + const basename = path.basename(filename, ".ts"); + let contents = await Bun.file(filename).text(); + + contents = applyGlobalReplacements(contents); + + // first approach doesnt work perfectly because we actually need to split each function declaration + // and then compile those separately + + const consumeWhitespace = /^\s*/; + const consumeTopLevelContent = /^(\/\*|\/\/|type|import|interface|\$|export (?:async )?function|(?:async )?function)/; + const consumeEndOfType = /;|.(?=export|type|interface|\$|\/\/|\/\*|function)/; + + const functions: ParsedBuiltin[] = []; + let directives: Record<string, any> = {}; + const bundledFunctions: BundledBuiltin[] = []; + let internal = false; + + while (contents.length) { + contents = contents.replace(consumeWhitespace, ""); + if (!contents.length) break; + const match = contents.match(consumeTopLevelContent); + if (!match) { + throw new SyntaxError("Could not process input:\n" + contents.slice(0, contents.indexOf("\n"))); + } + contents = contents.slice(match.index!); + if (match[1] === "import") { + // TODO: we may want to do stuff with these + const i = contents.indexOf(";"); + contents = contents.slice(i + 1); + } else if (match[1] === "/*") { + const i = contents.indexOf("*/") + 2; + internal ||= contents.slice(0, i).includes("@internal"); + contents = contents.slice(i); + } else if (match[1] === "//") { + const i = contents.indexOf("\n") + 1; + internal ||= contents.slice(0, i).includes("@internal"); + contents = contents.slice(i); + } else if (match[1] === "type" || match[1] === "export type") { + const i = contents.search(consumeEndOfType); + contents = contents.slice(i + 1); + } else if (match[1] === "interface") { + contents = sliceSourceCode(contents, false).rest; + } else if (match[1] === "$") { + const directive = contents.match(/^\$([a-zA-Z0-9]+)(?:\s*=\s*([^\n]+?))?\s*;?\n/); + if (!directive) { + throw new SyntaxError("Could not parse directive:\n" + contents.slice(0, contents.indexOf("\n"))); + } + const name = directive[1]; + let value; + try { + value = directive[2] ? JSON.parse(directive[2]) : true; + } catch (error) { + throw new SyntaxError("Could not parse directive value " + directive[2] + " (must be JSON parsable)"); + } + if (name === "constructor") { + throw new SyntaxError("$constructor not implemented"); + } + if (name === "nakedConstructor") { + throw new SyntaxError("$nakedConstructor not implemented"); + } + directives[name] = value; + contents = contents.slice(directive[0].length); + } else if (match[1] === "export function" || match[1] === "export async function") { + const declaration = contents.match( + /^export\s+(async\s+)?function\s+([a-zA-Z0-9]+)\s*\(([^)]*)\)(?:\s*:\s*([^{\n]+))?\s*{?/, + ); + if (!declaration) + throw new SyntaxError("Could not parse function declaration:\n" + contents.slice(0, contents.indexOf("\n"))); + + const async = !!declaration[1]; + const name = declaration[2]; + const paramString = declaration[3]; + const params = + paramString.trim().length === 0 ? [] : paramString.split(",").map(x => x.replace(/:.+$/, "").trim()); + if (params[0] === "this") { + params.shift(); + } + + const { result, rest } = sliceSourceCode(contents.slice(declaration[0].length - 1), true); + functions.push({ + name, + params, + directives, + source: result.trim().slice(1, -1), + async, + }); + contents = rest; + directives = {}; + } else if (match[1] === "function" || match[1] === "async function") { + const fnname = contents.match(/^function ([a-zA-Z0-9]+)\(([^)]*)\)(?:\s*:\s*([^{\n]+))?\s*{?/)![1]; + throw new SyntaxError("All top level functions must be exported: " + fnname); + } else { + throw new Error("TODO: parse " + match[1]); + } + } + + for (const fn of functions) { + const tmpFile = path.join(TMP_DIR, `${basename}.${fn.name}.ts`); + + // not sure if this optimization works properly in jsc builtins + // const useThis = fn.usesThis; + const useThis = true; + + // TODO: we should use format=IIFE so we could bundle imports and extra functions. + await Bun.write( + tmpFile, + `// @ts-nocheck +// GENERATED TEMP FILE - DO NOT EDIT +// Sourced from ${path.relative(TMP_DIR, filename)} + +// do not allow the bundler to rename a symbol to $ +($); + +$$capture_start$$(${fn.async ? "async " : ""}${ + useThis + ? `function(${fn.params.join(",")})` + : `${fn.params.length === 1 ? fn.params[0] : `(${fn.params.join(",")})`}=>` + } {${fn.source}}).$$capture_end$$; +`, + ); + await Bun.sleep(1); + const build = await Bun.build({ + entrypoints: [tmpFile], + define, + minify: true, + }); + if (!build.success) { + throw new AggregateError(build.logs, "Failed bundling builtin function " + fn.name + " from " + basename + ".ts"); + } + if (build.outputs.length !== 1) { + throw new Error("expected one output"); + } + const output = await build.outputs[0].text(); + const captured = output.match(/\$\$capture_start\$\$([\s\S]+)\.\$\$capture_end\$\$/)![1]; + const finalReplacement = + (fn.directives.sloppy ? captured : captured.replace(/function\s*\(.*?\)\s*{/, '$&"use strict";')) + .replace(/^\((async )?function\(/, "($1function (") + .replace(/__intrinsic__/g, "@") + "\n"; + + bundledFunctions.push({ + name: fn.name, + directives: fn.directives, + source: finalReplacement, + params: fn.params, + visibility: fn.directives.visibility ?? (fn.directives.linkTimeConstant ? "Private" : "Public"), + isGetter: !!fn.directives.getter, + isConstructor: !!fn.directives.constructor, + isLinkTimeConstant: !!fn.directives.linkTimeConstant, + isNakedConstructor: !!fn.directives.nakedConstructor, + intrinsic: fn.directives.intrinsic ?? "NoIntrinsic", + overriddenName: fn.directives.getter + ? `"get ${fn.name}"_s` + : fn.directives.overriddenName + ? `"${fn.directives.overriddenName}"_s` + : "ASCIILiteral()", + }); + } + + return { + functions: bundledFunctions, + internal, + }; +} + +const filesToProcess = readdirSync(SRC_DIR).filter(x => x.endsWith(".ts") && !x.endsWith(".d.ts")); + +const files: Array<{ basename: string; functions: BundledBuiltin[]; internal: boolean }> = []; +async function processFile(x: string) { + const basename = path.basename(x, ".ts"); + try { + files.push({ + basename, + ...(await processFileSplit(path.join(SRC_DIR, x))), + }); + } catch (error) { + console.error("Failed to process file: " + basename + ".ts"); + console.error(error); + process.exit(1); + } +} + +// Bun seems to crash if this is parallelized, :( +if (PARALLEL) { + await Promise.all(filesToProcess.map(processFile)); +} else { + for (const x of filesToProcess) { + await processFile(x); + } +} + +// C++ codegen +let bundledCPP = `// Generated by \`bun src/js/builtins/codegen\` +// Do not edit by hand. +namespace Zig { class GlobalObject; } +#include "root.h" +#include "config.h" +#include "JSDOMGlobalObject.h" +#include "WebCoreJSClientData.h" +#include <JavaScriptCore/JSObjectInlines.h> + +namespace WebCore { + +`; + +for (const { basename, functions } of files) { + bundledCPP += `/* ${basename}.ts */\n`; + const lowerBasename = low(basename); + for (const fn of functions) { + const name = `${lowerBasename}${cap(fn.name)}Code`; + bundledCPP += `// ${fn.name} +const JSC::ConstructAbility s_${name}ConstructAbility = JSC::ConstructAbility::CannotConstruct; +const JSC::ConstructorKind s_${name}ConstructorKind = JSC::ConstructorKind::None; +const JSC::ImplementationVisibility s_${name}ImplementationVisibility = JSC::ImplementationVisibility::${fn.visibility}; +const int s_${name}Length = ${fn.source.length}; +static const JSC::Intrinsic s_${name}Intrinsic = JSC::NoIntrinsic; +const char* const s_${name} = ${fmtCPPString(fn.source)}; + +`; + } + bundledCPP += `#define DEFINE_BUILTIN_GENERATOR(codeName, functionName, overriddenName, argumentCount) \\ +JSC::FunctionExecutable* codeName##Generator(JSC::VM& vm) \\ +{\\ + JSVMClientData* clientData = static_cast<JSVMClientData*>(vm.clientData); \\ + return clientData->builtinFunctions().${lowerBasename}Builtins().codeName##Executable()->link(vm, nullptr, clientData->builtinFunctions().${lowerBasename}Builtins().codeName##Source(), std::nullopt, s_##codeName##Intrinsic); \\ +} +WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_CODE(DEFINE_BUILTIN_GENERATOR) +#undef DEFINE_BUILTIN_GENERATOR + +`; +} + +bundledCPP += ` + +JSBuiltinInternalFunctions::JSBuiltinInternalFunctions(JSC::VM& vm) + : m_vm(vm) +`; + +for (const { basename, internal } of files) { + if (internal) { + bundledCPP += ` , m_${low(basename)}(vm)\n`; + } +} + +bundledCPP += ` +{ + UNUSED_PARAM(vm); +} + +template<typename Visitor> +void JSBuiltinInternalFunctions::visit(Visitor& visitor) +{ +`; +for (const { basename, internal } of files) { + if (internal) bundledCPP += ` m_${low(basename)}.visit(visitor);\n`; +} + +bundledCPP += ` + UNUSED_PARAM(visitor); +} + +template void JSBuiltinInternalFunctions::visit(AbstractSlotVisitor&); +template void JSBuiltinInternalFunctions::visit(SlotVisitor&); + +SUPPRESS_ASAN void JSBuiltinInternalFunctions::initialize(Zig::GlobalObject& globalObject) +{ + UNUSED_PARAM(globalObject); +`; + +for (const { basename, internal } of files) { + if (internal) { + bundledCPP += ` m_${low(basename)}.init(globalObject);\n`; + } +} + +bundledCPP += ` + JSVMClientData& clientData = *static_cast<JSVMClientData*>(m_vm.clientData); + Zig::GlobalObject::GlobalPropertyInfo staticGlobals[] = { +`; + +for (const { basename, internal } of files) { + if (internal) { + bundledCPP += `#define DECLARE_GLOBAL_STATIC(name) \\ + Zig::GlobalObject::GlobalPropertyInfo( \\ + clientData.builtinFunctions().${low(basename)}Builtins().name##PrivateName(), ${low( + basename, + )}().m_##name##Function.get() , JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly), + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_FUNCTION_NAME(DECLARE_GLOBAL_STATIC) + #undef DECLARE_GLOBAL_STATIC + `; + } +} + +bundledCPP += ` + }; + globalObject.addStaticGlobals(staticGlobals, std::size(staticGlobals)); + UNUSED_PARAM(clientData); +} + +} // namespace WebCore +`; + +// C++ Header codegen +let bundledHeader = `// Generated by \`bun src/js/builtins/codegen\` +// Do not edit by hand. +#pragma once +namespace Zig { class GlobalObject; } +#include "root.h" +#include <JavaScriptCore/BuiltinUtils.h> +#include <JavaScriptCore/Identifier.h> +#include <JavaScriptCore/JSFunction.h> +#include <JavaScriptCore/UnlinkedFunctionExecutable.h> +#include <JavaScriptCore/VM.h> +#include <JavaScriptCore/WeakInlines.h> + +namespace JSC { +class FunctionExecutable; +} + +namespace WebCore { +`; +for (const { basename, functions, internal } of files) { + bundledHeader += `/* ${basename}.ts */ +`; + const lowerBasename = low(basename); + + for (const fn of functions) { + const name = `${lowerBasename}${cap(fn.name)}Code`; + bundledHeader += `// ${fn.name} +#define WEBCORE_BUILTIN_${basename.toUpperCase()}_${fn.name.toUpperCase()} 1 +extern const char* const s_${name}; +extern const int s_${name}Length; +extern const JSC::ConstructAbility s_${name}ConstructAbility; +extern const JSC::ConstructorKind s_${name}ConstructorKind; +extern const JSC::ImplementationVisibility s_${name}ImplementationVisibility; + +`; + } + bundledHeader += `#define WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_DATA(macro) \\\n`; + for (const fn of functions) { + bundledHeader += ` macro(${fn.name}, ${lowerBasename}${cap(fn.name)}, ${fn.params.length}) \\\n`; + } + bundledHeader += "\n"; + bundledHeader += `#define WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_CODE(macro) \\\n`; + for (const fn of functions) { + const name = `${lowerBasename}${cap(fn.name)}Code`; + bundledHeader += ` macro(${name}, ${fn.name}, ${fn.overriddenName}, s_${name}Length) \\\n`; + } + bundledHeader += "\n"; + bundledHeader += `#define WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_FUNCTION_NAME(macro) \\\n`; + for (const fn of functions) { + bundledHeader += ` macro(${fn.name}) \\\n`; + } + bundledHeader += ` +#define DECLARE_BUILTIN_GENERATOR(codeName, functionName, overriddenName, argumentCount) \\ + JSC::FunctionExecutable* codeName##Generator(JSC::VM&); + +WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_CODE(DECLARE_BUILTIN_GENERATOR) +#undef DECLARE_BUILTIN_GENERATOR + +class ${basename}BuiltinsWrapper : private JSC::WeakHandleOwner { +public: + explicit ${basename}BuiltinsWrapper(JSC::VM& vm) + : m_vm(vm) + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_FUNCTION_NAME(INITIALIZE_BUILTIN_NAMES) +#define INITIALIZE_BUILTIN_SOURCE_MEMBERS(name, functionName, overriddenName, length) , m_##name##Source(JSC::makeSource(StringImpl::createWithoutCopying(s_##name, length), { })) + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_CODE(INITIALIZE_BUILTIN_SOURCE_MEMBERS) +#undef INITIALIZE_BUILTIN_SOURCE_MEMBERS + { + } + +#define EXPOSE_BUILTIN_EXECUTABLES(name, functionName, overriddenName, length) \\ + JSC::UnlinkedFunctionExecutable* name##Executable(); \\ + const JSC::SourceCode& name##Source() const { return m_##name##Source; } + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_CODE(EXPOSE_BUILTIN_EXECUTABLES) +#undef EXPOSE_BUILTIN_EXECUTABLES + + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_FUNCTION_NAME(DECLARE_BUILTIN_IDENTIFIER_ACCESSOR) + + void exportNames(); + +private: + JSC::VM& m_vm; + + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_FUNCTION_NAME(DECLARE_BUILTIN_NAMES) + +#define DECLARE_BUILTIN_SOURCE_MEMBERS(name, functionName, overriddenName, length) \\ + JSC::SourceCode m_##name##Source;\\ + JSC::Weak<JSC::UnlinkedFunctionExecutable> m_##name##Executable; + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_CODE(DECLARE_BUILTIN_SOURCE_MEMBERS) +#undef DECLARE_BUILTIN_SOURCE_MEMBERS + +}; + +#define DEFINE_BUILTIN_EXECUTABLES(name, functionName, overriddenName, length) \\ +inline JSC::UnlinkedFunctionExecutable* ${basename}BuiltinsWrapper::name##Executable() \\ +{\\ + if (!m_##name##Executable) {\\ + JSC::Identifier executableName = functionName##PublicName();\\ + if (overriddenName)\\ + executableName = JSC::Identifier::fromString(m_vm, overriddenName);\\ + m_##name##Executable = JSC::Weak<JSC::UnlinkedFunctionExecutable>(JSC::createBuiltinExecutable(m_vm, m_##name##Source, executableName, s_##name##ImplementationVisibility, s_##name##ConstructorKind, s_##name##ConstructAbility), this, &m_##name##Executable);\\ + }\\ + return m_##name##Executable.get();\\ +} +WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_CODE(DEFINE_BUILTIN_EXECUTABLES) +#undef DEFINE_BUILTIN_EXECUTABLES + +inline void ${basename}BuiltinsWrapper::exportNames() +{ +#define EXPORT_FUNCTION_NAME(name) m_vm.propertyNames->appendExternalName(name##PublicName(), name##PrivateName()); + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_FUNCTION_NAME(EXPORT_FUNCTION_NAME) +#undef EXPORT_FUNCTION_NAME +} +`; + + if (internal) { + bundledHeader += `class ${basename}BuiltinFunctions { +public: + explicit ${basename}BuiltinFunctions(JSC::VM& vm) : m_vm(vm) { } + + void init(JSC::JSGlobalObject&); + template<typename Visitor> void visit(Visitor&); + +public: + JSC::VM& m_vm; + +#define DECLARE_BUILTIN_SOURCE_MEMBERS(functionName) \\ + JSC::WriteBarrier<JSC::JSFunction> m_##functionName##Function; + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_FUNCTION_NAME(DECLARE_BUILTIN_SOURCE_MEMBERS) +#undef DECLARE_BUILTIN_SOURCE_MEMBERS +}; + +inline void ${basename}BuiltinFunctions::init(JSC::JSGlobalObject& globalObject) +{ +#define EXPORT_FUNCTION(codeName, functionName, overriddenName, length) \\ + m_##functionName##Function.set(m_vm, &globalObject, JSC::JSFunction::create(m_vm, codeName##Generator(m_vm), &globalObject)); + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_CODE(EXPORT_FUNCTION) +#undef EXPORT_FUNCTION +} + +template<typename Visitor> +inline void ${basename}BuiltinFunctions::visit(Visitor& visitor) +{ +#define VISIT_FUNCTION(name) visitor.append(m_##name##Function); + WEBCORE_FOREACH_${basename.toUpperCase()}_BUILTIN_FUNCTION_NAME(VISIT_FUNCTION) +#undef VISIT_FUNCTION +} + +template void ${basename}BuiltinFunctions::visit(JSC::AbstractSlotVisitor&); +template void ${basename}BuiltinFunctions::visit(JSC::SlotVisitor&); + `; + } +} +bundledHeader += `class JSBuiltinFunctions { +public: + explicit JSBuiltinFunctions(JSC::VM& vm) + : m_vm(vm) +`; + +for (const { basename } of files) { + bundledHeader += ` , m_${low(basename)}Builtins(m_vm)\n`; +} + +bundledHeader += ` + { +`; + +for (const { basename, internal } of files) { + if (internal) { + bundledHeader += ` m_${low(basename)}Builtins.exportNames();\n`; + } +} + +bundledHeader += ` } +`; + +for (const { basename } of files) { + bundledHeader += ` ${basename}BuiltinsWrapper& ${low(basename)}Builtins() { return m_${low( + basename, + )}Builtins; }\n`; +} + +bundledHeader += ` +private: + JSC::VM& m_vm; +`; + +for (const { basename } of files) { + bundledHeader += ` ${basename}BuiltinsWrapper m_${low(basename)}Builtins;\n`; +} + +bundledHeader += `; +}; + +class JSBuiltinInternalFunctions { +public: + explicit JSBuiltinInternalFunctions(JSC::VM&); + + template<typename Visitor> void visit(Visitor&); + void initialize(Zig::GlobalObject&); +`; + +for (const { basename, internal } of files) { + if (internal) { + bundledHeader += ` ${basename}BuiltinFunctions& ${low(basename)}() { return m_${low(basename)}; }\n`; + } +} + +bundledHeader += ` +private: + JSC::VM& m_vm; +`; + +for (const { basename, internal } of files) { + if (internal) { + bundledHeader += ` ${basename}BuiltinFunctions m_${low(basename)};\n`; + } +} + +bundledHeader += ` +}; + +} // namespace WebCore +`; + +await Bun.write(path.join(OUT_DIR, "WebCoreJSBuiltins.h"), bundledHeader); +await Bun.write(path.join(OUT_DIR, "WebCoreJSBuiltins.cpp"), bundledCPP); + +// Generate TS types +let dts = `// Generated by \`bun src/js/builtins/codegen\` +// Do not edit by hand. +type RemoveThis<F> = F extends (this: infer T, ...args: infer A) => infer R ? (...args: A) => R : F; +`; + +for (const { basename, functions, internal } of files) { + if (internal) { + dts += `\n// ${basename}.ts\n`; + for (const fn of functions) { + dts += `declare const \$${fn.name}: RemoveThis<typeof import("${path.relative( + OUT_DIR, + path.join(SRC_DIR, basename), + )}")[${JSON.stringify(fn.name)}]>;\n`; + } + } +} + +await Bun.write(path.join(OUT_DIR, "WebCoreJSBuiltins.d.ts"), dts); + +const totalJSSize = files.reduce( + (acc, { functions }) => acc + functions.reduce((acc, fn) => acc + fn.source.length, 0), + 0, +); + +if (!KEEP_TMP) { + await rmSync(TMP_DIR, { recursive: true }); +} + +console.log( + `Embedded JS size: %s bytes (across %s functions, %s files)`, + totalJSSize, + files.reduce((acc, { functions }) => acc + functions.length, 0), + files.length, +); +console.log(`[${performance.now().toFixed(1)}ms]`); diff --git a/src/js/builtins/codegen/replacements.ts b/src/js/builtins/codegen/replacements.ts new file mode 100644 index 000000000..05c81b901 --- /dev/null +++ b/src/js/builtins/codegen/replacements.ts @@ -0,0 +1,100 @@ +import { LoaderKeys } from "../../../api/schema"; + +// This is a list of extra syntax replacements to do. Kind of like macros +// These are only run on code itself, not string contents or comments. +export const replacements: ReplacementRule[] = [ + { from: /\bthrow new TypeError\b/g, to: "$throwTypeError" }, + { from: /\bthrow new RangeError\b/g, to: "$throwRangeError" }, + { from: /\bthrow new OutOfMemoryError\b/g, to: "$throwOutOfMemoryError" }, + { from: /\bnew TypeError\b/g, to: "$makeTypeError" }, +]; + +// These rules are run on the entire file, including within strings. +export const globalReplacements: ReplacementRule[] = [ + { + from: /\bnotImplementedIssue\(\s*([0-9]+)\s*,\s*((?:"[^"]*"|'[^']+'))\s*\)/g, + to: "new TypeError(`${$2} is not implemented yet. See https://github.com/oven-sh/bun/issues/$1`)", + }, + { + from: /\bnotImplementedIssueFn\(\s*([0-9]+)\s*,\s*((?:"[^"]*"|'[^']+'))\s*\)/g, + to: "() => $throwTypeError(`${$2} is not implemented yet. See https://github.com/oven-sh/bun/issues/$1`)", + }, +]; + +// This is a list of globals we should access using @ notation +// undefined -> __intrinsic__undefined -> @undefined +export const globalsToPrefix = [ + "AbortSignal", + "Array", + "ArrayBuffer", + "Buffer", + "Bun", + "Infinity", + "Loader", + "Promise", + "ReadableByteStreamController", + "ReadableStream", + "ReadableStreamBYOBReader", + "ReadableStreamBYOBRequest", + "ReadableStreamDefaultController", + "ReadableStreamDefaultReader", + "TransformStream", + "TransformStreamDefaultController", + "Uint8Array", + "WritableStream", + "WritableStreamDefaultController", + "WritableStreamDefaultWriter", + "isFinite", + "isNaN", + "undefined", +]; + +// These enums map to $<enum>IdToLabel and $<enum>LabelToId +// Make sure to define in ./builtins.d.ts +export const enums = { + Loader: LoaderKeys, + ImportKind: [ + "entry-point", + "import-statement", + "require-call", + "dynamic-import", + "require-resolve", + "import-rule", + "url-token", + "internal", + ], +}; + +// These identifiers have typedef but not present at runtime (converted with replacements) +// If they are present in the bundle after runtime, we warn at the user. +// TODO: implement this check. +export const warnOnIdentifiersNotPresentAtRuntime = [ + // + "OutOfMemoryError", + "notImplementedIssue", + "notImplementedIssueFn", +]; + +export interface ReplacementRule { + from: RegExp; + to: string; + global?: boolean; +} + +/** Applies source code replacements as defined in `replacements` */ +export function applyReplacements(src: string) { + let result = src.replace(/\$([a-zA-Z0-9_]+)\b/gm, `__intrinsic__$1`); + for (const replacement of replacements) { + result = result.replace(replacement.from, replacement.to.replaceAll("$", "__intrinsic__")); + } + return result; +} + +/** Applies source code replacements as defined in `globalReplacements` */ +export function applyGlobalReplacements(src: string) { + let result = src; + for (const replacement of globalReplacements) { + result = result.replace(replacement.from, replacement.to.replaceAll("$", "__intrinsic__")); + } + return result; +} diff --git a/src/js/builtins/tsconfig.json b/src/js/builtins/tsconfig.json new file mode 100644 index 000000000..f8b12c5c3 --- /dev/null +++ b/src/js/builtins/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true + }, + "include": [".", "../private.d.ts", "builtins.d.ts", "../generated/builtins/WebCoreJSBuiltins.d.ts", "../../../packages/bun-types/index.d.ts"] +} |