diff --git a/examples/spec/diagnostics_channel.js b/examples/spec/diagnostics_channel.js new file mode 100644 index 0000000..7f8c49b --- /dev/null +++ b/examples/spec/diagnostics_channel.js @@ -0,0 +1,106 @@ +import { test, testThrows, testDeep, summary } from './helpers.js'; +import diagnosticsChannel from 'node:diagnostics_channel'; + +console.log('diagnostics_channel\n'); + +function collectEvents(tracing) { + const events = []; + + for (const name of ['start', 'end', 'asyncStart', 'asyncEnd', 'error']) { + tracing[name].subscribe(message => { + events.push({ name, message: { ...message } }); + }); + } + + return events; +} + +test('exports channel', typeof diagnosticsChannel.channel, 'function'); +test('exports tracingChannel', typeof diagnosticsChannel.tracingChannel, 'function'); + +const symbolName = Symbol('diagnostics'); +test( + 'channel caches string names', + diagnosticsChannel.channel('spec:diagnostics:string') === diagnosticsChannel.channel('spec:diagnostics:string'), + true +); +test('channel accepts symbol names', diagnosticsChannel.channel(symbolName).name, symbolName); + +testThrows('channel rejects invalid names', () => diagnosticsChannel.channel(123)); +testThrows('subscribe rejects non-functions', () => diagnosticsChannel.channel('spec:diagnostics:invalid-subscription').subscribe(123)); + +const storeChannel = diagnosticsChannel.channel('spec:diagnostics:store'); +const store = { + run(_value, fn) { + return fn(); + } +}; +test('hasSubscribers starts false', storeChannel.hasSubscribers, false); +storeChannel.bindStore(store); +test('bindStore contributes to hasSubscribers', storeChannel.hasSubscribers, true); +storeChannel.unbindStore(store); +test('unbindStore clears hasSubscribers', storeChannel.hasSubscribers, false); + +const snapshotChannel = diagnosticsChannel.channel('spec:diagnostics:snapshot'); +const snapshotCalls = []; +const lateSubscriber = () => snapshotCalls.push('late'); +snapshotChannel.subscribe(() => { + snapshotCalls.push('first'); + snapshotChannel.subscribe(lateSubscriber); +}); +snapshotChannel.subscribe(() => snapshotCalls.push('second')); +snapshotChannel.publish({}); +snapshotChannel.publish({}); +testDeep('publish snapshots subscribers', snapshotCalls, ['first', 'second', 'first', 'second', 'late']); + +const syncTracing = diagnosticsChannel.tracingChannel('spec:diagnostics:sync'); +const syncEvents = collectEvents(syncTracing); +test( + 'traceSync returns result', + syncTracing.traceSync(() => 42, { phase: 'sync' }), + 42 +); +testDeep('traceSync publishes start/end', syncEvents, [ + { name: 'start', message: { phase: 'sync' } }, + { name: 'end', message: { phase: 'sync', result: 42 } } +]); + +const promiseTracing = diagnosticsChannel.tracingChannel('spec:diagnostics:promise'); +const promiseEvents = collectEvents(promiseTracing); +test('tracePromise resolves result', await promiseTracing.tracePromise(() => Promise.resolve(7), { phase: 'promise' }), 7); +testDeep('tracePromise publishes end before async events', promiseEvents, [ + { name: 'start', message: { phase: 'promise' } }, + { name: 'end', message: { phase: 'promise' } }, + { name: 'asyncStart', message: { phase: 'promise', result: 7 } }, + { name: 'asyncEnd', message: { phase: 'promise', result: 7 } } +]); + +const callbackTracing = diagnosticsChannel.tracingChannel('spec:diagnostics:callback'); +const callbackEvents = collectEvents(callbackTracing); +let callbackError = ''; +const callbackReturn = callbackTracing.traceCallback( + callback => { + try { + callback(null, 9); + } catch (err) { + callbackError = err.message; + } + return 'outer-result'; + }, + -1, + { phase: 'callback' }, + undefined, + () => { + throw new Error('callback boom'); + } +); +test('traceCallback returns outer result', callbackReturn, 'outer-result'); +test('traceCallback uses the last argument by default', callbackError, 'callback boom'); +testDeep('traceCallback still publishes asyncEnd when callback throws', callbackEvents, [ + { name: 'start', message: { phase: 'callback' } }, + { name: 'asyncStart', message: { phase: 'callback', result: 9 } }, + { name: 'asyncEnd', message: { phase: 'callback', result: 9 } }, + { name: 'end', message: { phase: 'callback', result: 9 } } +]); + +summary(); diff --git a/include/common.h b/include/common.h index 8a3cf3f..04d7fc1 100644 --- a/include/common.h +++ b/include/common.h @@ -117,7 +117,8 @@ typedef enum { BRAND_WASM_TABLE, BRAND_WASM_TAG, BRAND_WASM_EXCEPTION, - BRAND_DATE + BRAND_DATE, + BRAND_MODULE_NAMESPACE } object_brand_id_t; static inline void *mantissa_chk(void *p, const char *func) { diff --git a/include/modules/io.h b/include/modules/io.h index d03a595..44807e5 100644 --- a/include/modules/io.h +++ b/include/modules/io.h @@ -29,6 +29,8 @@ typedef struct { #define C_RED C("\x1b[31m") void init_console_module(void); +ant_value_t console_library(ant_t *js); + void print_value_colored(const char *str, FILE *stream); void print_repl_value(ant_t *js, ant_value_t val, FILE *stream); bool print_uncaught_throw(ant_t *js); diff --git a/include/modules/stream.h b/include/modules/stream.h new file mode 100644 index 0000000..56e53b4 --- /dev/null +++ b/include/modules/stream.h @@ -0,0 +1,23 @@ +#ifndef ANT_STREAM_MODULE_H +#define ANT_STREAM_MODULE_H + +#include "types.h" + +void stream_init_constructors(ant_t *js); + +ant_value_t stream_library(ant_t *js); +ant_value_t stream_promises_library(ant_t *js); + +ant_value_t stream_readable_constructor(ant_t *js); +ant_value_t stream_writable_constructor(ant_t *js); +ant_value_t stream_readable_prototype(ant_t *js); +ant_value_t stream_writable_prototype(ant_t *js); + +ant_value_t stream_construct_readable(ant_t *js, ant_value_t base_proto, ant_value_t options); +ant_value_t stream_construct_writable(ant_t *js, ant_value_t base_proto, ant_value_t options); +ant_value_t stream_readable_push(ant_t *js, ant_value_t stream_obj, ant_value_t chunk, ant_value_t encoding); + +void stream_init_readable_object(ant_t *js, ant_value_t obj, ant_value_t options); +void stream_init_writable_object(ant_t *js, ant_value_t obj, ant_value_t options); + +#endif diff --git a/include/modules/timer.h b/include/modules/timer.h index 2215cd8..4de1850 100644 --- a/include/modules/timer.h +++ b/include/modules/timer.h @@ -3,6 +3,7 @@ #include "types.h" +ant_value_t timers_library(ant_t *js); ant_value_t timers_promises_library(ant_t *js); void init_timer_module(void); diff --git a/include/modules/util.h b/include/modules/util.h index 0664ae7..b6d1b37 100644 --- a/include/modules/util.h +++ b/include/modules/util.h @@ -4,5 +4,6 @@ #include "types.h" ant_value_t util_library(ant_t *js); +ant_value_t util_types_library(ant_t *js); #endif diff --git a/src/builtins/node/diagnostics_channel.mjs b/src/builtins/node/diagnostics_channel.mjs new file mode 100644 index 0000000..abcd7a0 --- /dev/null +++ b/src/builtins/node/diagnostics_channel.mjs @@ -0,0 +1,242 @@ +const registry = new Map(); + +const TRACING_EVENTS = ['start', 'end', 'asyncStart', 'asyncEnd', 'error']; + +function createInvalidArgType(name, expected) { + const error = new TypeError(`The "${name}" argument must be of type ${expected}.`); + error.code = 'ERR_INVALID_ARG_TYPE'; + return error; +} + +function validateFunction(value, name) { + if (typeof value !== 'function') throw createInvalidArgType(name, 'function'); +} + +function validateChannelName(name) { + if (typeof name === 'string' || typeof name === 'symbol') return; + throw createInvalidArgType('channel', 'string or symbol'); +} + +function normalizeIndex(length, index) { + if (index < 0) return Math.max(length + index, 0); + return index; +} + +function createTracingChannels(name) { + const prefix = `tracing:${name}`; + return { + start: channel(`${prefix}:start`), + end: channel(`${prefix}:end`), + asyncStart: channel(`${prefix}:asyncStart`), + asyncEnd: channel(`${prefix}:asyncEnd`), + error: channel(`${prefix}:error`) + }; +} + +export class Channel { + constructor(name) { + this.name = name; + this._subscribers = []; + this._stores = undefined; + } + + get hasSubscribers() { + return this._subscribers.length > 0 || !!(this._stores && this._stores.size > 0); + } + + subscribe(fn) { + validateFunction(fn, 'subscription'); + this._subscribers.push(fn); + } + + unsubscribe(fn) { + const index = this._subscribers.indexOf(fn); + if (index === -1) return false; + this._subscribers.splice(index, 1); + return true; + } + + publish(message) { + const subscribers = this._subscribers.slice(); + for (const fn of subscribers) fn(message, this.name); + } + + bindStore(store, transform) { + if (!this._stores) this._stores = new Map(); + this._stores.set(store, transform); + } + + unbindStore(store) { + if (!this._stores) return false; + return this._stores.delete(store); + } + + runStores(context, fn, thisArg, ...args) { + if (!this._stores || this._stores.size === 0) { + this.publish(context); + return fn.apply(thisArg, args); + } + + const entries = Array.from(this._stores.entries()); + const run = index => { + if (index === entries.length) { + this.publish(context); + return fn.apply(thisArg, args); + } + + const [store, transform] = entries[index]; + return store.run(transform ? transform(context) : context, () => run(index + 1)); + }; + + return run(0); + } +} + +export class TracingChannel { + constructor(channels) { + for (const event of TRACING_EVENTS) this[event] = channels[event]; + } + + get hasSubscribers() { + return TRACING_EVENTS.some(event => this[event].hasSubscribers); + } + + subscribe(subscribers) { + for (const event of TRACING_EVENTS) if (subscribers[event] !== undefined) this[event].subscribe(subscribers[event]); + } + + unsubscribe(subscribers) { + let ok = true; + + for (const event of TRACING_EVENTS) { + if (subscribers[event] !== undefined && !this[event].unsubscribe(subscribers[event])) ok = false; + } + + return ok; + } + + traceSync(fn, context = {}, thisArg, ...args) { + if (!this.hasSubscribers) return fn.apply(thisArg, args); + + const { start, end, error } = this; + + return start.runStores(context, () => { + try { + const result = fn.apply(thisArg, args); + context.result = result; + end.publish(context); + return result; + } catch (err) { + context.error = err; + error.publish(context); + end.publish(context); + throw err; + } + }); + } + + tracePromise(fn, context = {}, thisArg, ...args) { + if (!this.hasSubscribers) return fn.apply(thisArg, args); + const { start, end, asyncStart, asyncEnd, error } = this; + + const resolve = result => { + context.result = result; + asyncStart.publish(context); + asyncEnd.publish(context); + return result; + }; + + const reject = err => { + context.error = err; + error.publish(context); + asyncStart.publish(context); + asyncEnd.publish(context); + return Promise.reject(err); + }; + + return start.runStores(context, () => { + try { + let promise = fn.apply(thisArg, args); + if (!(promise instanceof Promise)) promise = Promise.resolve(promise); + const wrapped = promise.then(resolve, reject); + end.publish(context); + return wrapped; + } catch (err) { + context.error = err; + error.publish(context); + end.publish(context); + throw err; + } + }); + } + + traceCallback(fn, position = -1, context = {}, thisArg, ...args) { + if (!this.hasSubscribers) return fn.apply(thisArg, args); + + const { start, end, asyncStart, asyncEnd, error } = this; + const callbackIndex = normalizeIndex(args.length, position); + const callback = args[callbackIndex]; + + validateFunction(callback, 'callback'); + + args.splice(callbackIndex, 1, function wrappedCallback(err, result) { + if (err) { + context.error = err; + error.publish(context); + } else context.result = result; + + return asyncStart.runStores(context, () => { + try { + return callback.apply(this, arguments); + } finally { + asyncEnd.publish(context); + } + }); + }); + + return start.runStores(context, () => { + try { + const result = fn.apply(thisArg, args); + end.publish(context); + return result; + } catch (err) { + context.error = err; + error.publish(context); + end.publish(context); + throw err; + } + }); + } +} + +export function channel(name) { + validateChannelName(name); + + let ch = registry.get(name); + if (!ch) { + ch = new Channel(name); + registry.set(name, ch); + } + + return ch; +} + +export function subscribe(name, fn) { + channel(name).subscribe(fn); +} + +export function unsubscribe(name, fn) { + return channel(name).unsubscribe(fn); +} + +export function hasSubscribers(name) { + const ch = registry.get(name); + return ch ? ch.hasSubscribers : false; +} + +export function tracingChannel(nameOrChannels) { + if (typeof nameOrChannels === 'string') nameOrChannels = createTracingChannels(nameOrChannels); + return new TracingChannel(nameOrChannels); +} + +export default { channel, subscribe, unsubscribe, hasSubscribers, tracingChannel, Channel, TracingChannel }; diff --git a/src/builtins/node/stream.cjs b/src/builtins/node/stream.cjs deleted file mode 100644 index d84a7f6..0000000 --- a/src/builtins/node/stream.cjs +++ /dev/null @@ -1,563 +0,0 @@ -'use strict'; - -var EventEmitter = require('node:events').EventEmitter; -var BufferCtor = typeof Buffer !== 'undefined' ? Buffer : require('node:buffer').Buffer; - -function noop() {} - -function nextTick(fn) { - if (typeof process !== 'undefined' && process && typeof process.nextTick === 'function') { - var args = Array.prototype.slice.call(arguments, 1); - process.nextTick(function () { - fn.apply(undefined, args); - }); - return; - } - - Promise.resolve().then(function () { - fn(); - }); -} - -function once(fn) { - var called = false; - return function () { - if (called) return; - called = true; - return fn.apply(this, arguments); - }; -} - -function inherits(target, source) { - Object.getOwnPropertyNames(source).forEach(function (name) { - if (name === 'constructor') return; - if (Object.prototype.hasOwnProperty.call(target, name)) return; - Object.defineProperty(target, name, Object.getOwnPropertyDescriptor(source, name)); - }); -} - -function normalizeChunk(chunk, objectMode, encoding) { - if (objectMode || chunk == null || BufferCtor.isBuffer(chunk) || chunk instanceof Uint8Array) return chunk; - if (typeof chunk === 'string') return BufferCtor.from(chunk, encoding || 'utf8'); - return BufferCtor.from(String(chunk), encoding || 'utf8'); -} - -function toAsyncIterable(source) { - if (source && typeof source[Symbol.asyncIterator] === 'function') return source; - if (source && typeof source[Symbol.iterator] === 'function') { - return { - async *[Symbol.asyncIterator]() { - for (var value of source) yield value; - } - }; - } - - if (source && typeof source.getReader === 'function') { - return { - async *[Symbol.asyncIterator]() { - var reader = source.getReader(); - try { - while (true) { - var result = await reader.read(); - if (!result || result.done) break; - yield result.value; - } - } finally { - if (reader.releaseLock) reader.releaseLock(); - } - } - }; - } - - return { - async *[Symbol.asyncIterator]() { - if (source !== undefined) yield source; - } - }; -} - -function Stream(options) { - if (!(this instanceof Stream)) return new Stream(options); - this.readable = true; - this.writable = true; - this.destroyed = false; - this._paused = false; - this._pipes = []; - this._streamOptions = options || {}; -} - -Stream.prototype = Object.create(EventEmitter.prototype); -Stream.prototype.constructor = Stream; - -Stream.prototype.pipe = function (dest, options) { - var source = this; - var end = !options || options.end !== false; - - function onData(chunk) { - if (!dest || typeof dest.write !== 'function') return; - var ok = dest.write(chunk); - if (ok === false && typeof source.pause === 'function') source.pause(); - } - - function onDrain() { - if (typeof source.resume === 'function') source.resume(); - } - - function onEnd() { - cleanup(); - if (end && dest && typeof dest.end === 'function') dest.end(); - } - - function onClose() { - cleanup(); - } - - function onError(err) { - cleanup(); - if (dest && typeof dest.emit === 'function' && dest.listenerCount('error') > 0) dest.emit('error', err); - } - - function cleanup() { - source.removeListener('data', onData); - source.removeListener('end', onEnd); - source.removeListener('close', onClose); - source.removeListener('error', onError); - if (dest && typeof dest.removeListener === 'function') dest.removeListener('drain', onDrain); - source._pipes = source._pipes.filter(function (entry) { - return entry.dest !== dest; - }); - } - - this._pipes.push({ dest: dest, cleanup: cleanup }); - this.on('data', onData); - this.once('end', onEnd); - this.once('close', onClose); - this.on('error', onError); - if (dest && typeof dest.on === 'function') dest.on('drain', onDrain); - if (dest && typeof dest.emit === 'function') dest.emit('pipe', this); - if (typeof this.resume === 'function') this.resume(); - return dest; -}; - -Stream.prototype.unpipe = function (dest) { - var pipes = this._pipes.slice(); - for (var i = 0; i < pipes.length; i++) { - if (!dest || pipes[i].dest === dest) pipes[i].cleanup(); - } - return this; -}; - -Stream.prototype.pause = function () { - this._paused = true; - this.emit('pause'); - return this; -}; - -Stream.prototype.resume = function () { - this._paused = false; - this.emit('resume'); - return this; -}; - -Stream.prototype.isPaused = function () { - return !!this._paused; -}; - -Stream.prototype.destroy = function (err) { - if (this.destroyed) return this; - - var self = this; - this.destroyed = true; - - function done(destroyErr) { - if (destroyErr) self.emit('error', destroyErr); - self.emit('close'); - } - - if (typeof this._destroy === 'function') { - this._destroy(err || null, once(done)); - } else done(err || null); - - return this; -}; - -function Readable(options) { - Stream.call(this, options); - options = options || {}; - this.readable = true; - this.writable = false; - this.readableEnded = false; - this._readableState = { - objectMode: !!options.objectMode, - ended: false, - endEmitted: false, - flowing: false, - reading: false, - highWaterMark: options.highWaterMark || 16384, - buffer: [] - }; - - if (typeof options.read === 'function') this._read = options.read; -} - -Readable.prototype = Object.create(Stream.prototype); -Readable.prototype.constructor = Readable; - -Readable.prototype._read = noop; - -Readable.prototype._flushReadable = function () { - while (this._readableState.flowing && this._readableState.buffer.length > 0) { - var chunk = this._readableState.buffer.shift(); - this.emit('data', chunk); - } - - if (this._readableState.ended && this._readableState.buffer.length === 0 && !this._readableState.endEmitted) { - this._readableState.endEmitted = true; - this.readableEnded = true; - this.emit('end'); - this.emit('close'); - } -}; - -Readable.prototype._maybeRead = function () { - if (this.destroyed || this._readableState.reading || this._readableState.ended) return; - if (this._readableState.buffer.length > 0) return; - - this._readableState.reading = true; - try { - this._read(this._readableState.highWaterMark); - } finally { - this._readableState.reading = false; - } -}; - -Readable.prototype.push = function (chunk, encoding) { - if (this.destroyed) return false; - - if (chunk === null) { - this._readableState.ended = true; - this._flushReadable(); - return false; - } - - this._readableState.buffer.push(normalizeChunk(chunk, this._readableState.objectMode, encoding)); - if (this._readableState.flowing) this._flushReadable(); - return this._readableState.flowing; -}; - -Readable.prototype.read = function () { - if (this._readableState.buffer.length === 0) this._maybeRead(); - if (this._readableState.buffer.length === 0) return null; - - var chunk = this._readableState.buffer.shift(); - if (this._readableState.flowing) this._flushReadable(); - return chunk; -}; - -Readable.prototype.on = function (event, listener) { - var result = Stream.prototype.on.call(this, event, listener); - if (event === 'data') this.resume(); - return result; -}; - -Readable.prototype.resume = function () { - this._readableState.flowing = true; - Stream.prototype.resume.call(this); - this._maybeRead(); - this._flushReadable(); - return this; -}; - -Readable.prototype.pause = function () { - this._readableState.flowing = false; - return Stream.prototype.pause.call(this); -}; - -Readable.from = function (source, options) { - var readable = new Readable(options); - - nextTick(function () { - (async function () { - try { - for await (var chunk of toAsyncIterable(source)) { - if (readable.destroyed) return; - readable.push(chunk); - } - readable.push(null); - } catch (err) { - readable.destroy(err); - } - })(); - }); - - return readable; -}; - -Readable.fromWeb = function (source, options) { - return Readable.from(source, options); -}; - -function Writable(options) { - Stream.call(this, options); - options = options || {}; - this.readable = false; - this.writable = true; - this.writableEnded = false; - this.writableFinished = false; - this._writableState = { - objectMode: !!options.objectMode || !!options.writableObjectMode, - finished: false, - ended: false - }; - - if (typeof options.write === 'function') this._write = options.write; - if (typeof options.final === 'function') this._final = options.final; - if (typeof options.destroy === 'function') this._destroy = options.destroy; -} - -Writable.prototype = Object.create(Stream.prototype); -Writable.prototype.constructor = Writable; - -Writable.prototype._write = function (_chunk, _encoding, callback) { - callback(); -}; - -Writable.prototype._final = function (callback) { - callback(); -}; - -Writable.prototype.write = function (chunk, encoding, callback) { - if (typeof encoding === 'function') { - callback = encoding; - encoding = undefined; - } - - if (this.writableEnded || this.destroyed) { - var writeErr = new Error('write after end'); - if (typeof callback === 'function') callback(writeErr); - else this.emit('error', writeErr); - return false; - } - - var self = this; - var done = once(function (err) { - if (err) { - self.destroy(err); - if (typeof callback === 'function') callback(err); - return; - } - if (typeof callback === 'function') callback(); - self.emit('drain'); - }); - - try { - this._write(normalizeChunk(chunk, this._writableState.objectMode, encoding), encoding || 'utf8', done); - } catch (err) { - done(err); - return false; - } - - return !this.destroyed; -}; - -Writable.prototype.end = function (chunk, encoding, callback) { - if (typeof chunk === 'function') { - callback = chunk; - chunk = undefined; - encoding = undefined; - } else if (typeof encoding === 'function') { - callback = encoding; - encoding = undefined; - } - - if (chunk !== undefined && chunk !== null) this.write(chunk, encoding); - if (this.writableEnded) { - if (typeof callback === 'function') callback(); - return this; - } - - var self = this; - this.writableEnded = true; - this._writableState.ended = true; - - this._final( - once(function (err) { - if (err) { - self.destroy(err); - if (typeof callback === 'function') callback(err); - return; - } - - self.writableFinished = true; - self._writableState.finished = true; - self.emit('finish'); - if (typeof callback === 'function') callback(); - if (!self.readable) self.emit('close'); - }) - ); - - return this; -}; - -Writable.prototype.cork = noop; -Writable.prototype.uncork = noop; - -function Duplex(options) { - Readable.call(this, options); - Writable.call(this, options); - options = options || {}; - this.readable = true; - this.writable = true; - this.allowHalfOpen = options.allowHalfOpen !== false; -} - -Duplex.prototype = Object.create(Readable.prototype); -Duplex.prototype.constructor = Duplex; -inherits(Duplex.prototype, Writable.prototype); - -function Transform(options) { - Duplex.call(this, options); - options = options || {}; - if (typeof options.transform === 'function') this._transform = options.transform; - if (typeof options.flush === 'function') this._flush = options.flush; -} - -Transform.prototype = Object.create(Duplex.prototype); -Transform.prototype.constructor = Transform; - -Transform.prototype._transform = function (chunk, _encoding, callback) { - callback(null, chunk); -}; - -Transform.prototype._write = function (chunk, encoding, callback) { - var self = this; - this._transform(chunk, encoding, function (err, data) { - if (err) { - callback(err); - return; - } - if (data !== undefined && data !== null) self.push(data); - callback(); - }); -}; - -Transform.prototype._final = function (callback) { - var self = this; - if (typeof this._flush === 'function') { - this._flush(function (err, data) { - if (err) { - callback(err); - return; - } - if (data !== undefined && data !== null) self.push(data); - self.push(null); - callback(); - }); - return; - } - - this.push(null); - callback(); -}; - -function PassThrough(options) { - Transform.call(this, options); -} - -PassThrough.prototype = Object.create(Transform.prototype); -PassThrough.prototype.constructor = PassThrough; - -PassThrough.prototype._transform = function (chunk, _encoding, callback) { - callback(null, chunk); -}; - -function finished(stream, callback) { - callback = once(typeof callback === 'function' ? callback : noop); - - function onFinish() { - cleanup(); - callback(); - } - - function onError(err) { - cleanup(); - callback(err); - } - - function cleanup() { - stream.removeListener('end', onFinish); - stream.removeListener('finish', onFinish); - stream.removeListener('close', onFinish); - stream.removeListener('error', onError); - } - - stream.on('end', onFinish); - stream.on('finish', onFinish); - stream.on('close', onFinish); - stream.on('error', onError); - return stream; -} - -function pipeline() { - var args = Array.prototype.slice.call(arguments); - var callback = typeof args[args.length - 1] === 'function' ? args.pop() : noop; - - if (args.length < 2) { - nextTick(callback); - return args[0]; - } - - var done = once(callback); - for (var i = 0; i < args.length - 1; i++) { - finished(args[i], function (err) { - if (err) done(err); - }); - args[i].pipe(args[i + 1]); - } - - finished(args[args.length - 1], done); - return args[args.length - 1]; -} - -var promises = { - pipeline: function () { - var args = Array.prototype.slice.call(arguments); - return new Promise(function (resolve, reject) { - args.push(function (err) { - if (err) reject(err); - else resolve(); - }); - pipeline.apply(undefined, args); - }); - }, - finished: function (stream) { - return new Promise(function (resolve, reject) { - finished(stream, function (err) { - if (err) reject(err); - else resolve(); - }); - }); - } -}; - -Stream.Stream = Stream; -Stream.Readable = Readable; -Stream.Writable = Writable; -Stream.Duplex = Duplex; -Stream.Transform = Transform; -Stream.PassThrough = PassThrough; -Stream.pipeline = pipeline; -Stream.finished = finished; -Stream.promises = promises; - -module.exports = Stream; -module.exports.default = Stream; -module.exports.Stream = Stream; -module.exports.Readable = Readable; -module.exports.Writable = Writable; -module.exports.Duplex = Duplex; -module.exports.Transform = Transform; -module.exports.PassThrough = PassThrough; -module.exports.pipeline = pipeline; -module.exports.finished = finished; -module.exports.promises = promises; diff --git a/src/builtins/node/stub.txt b/src/builtins/node/stub.txt index bf95cff..6ec1c68 100644 --- a/src/builtins/node/stub.txt +++ b/src/builtins/node/stub.txt @@ -4,4 +4,3 @@ the following builtins are currently compatibility stubs, not full node.js imple - https.mjs - http2.mjs - tls.mjs -- stream.cjs diff --git a/src/esm/loader.c b/src/esm/loader.c index f1f6223..14086f3 100644 --- a/src/esm/loader.c +++ b/src/esm/loader.c @@ -167,6 +167,12 @@ static ant_value_t esm_default_export_or_namespace(ant_t *js, ant_value_t ns) { return vtype(default_val) != T_UNDEF ? default_val : ns; } +static ant_value_t esm_make_namespace_object(ant_t *js) { + ant_value_t ns = js_mkobj(js); + js_set_slot(ns, SLOT_BRAND, js_mknum(BRAND_MODULE_NAMESPACE)); + return ns; +} + static ant_value_t esm_complete_value_module(esm_module_t *mod, ant_value_t value) { if (is_err(value)) { mod->is_loading = false; @@ -1020,7 +1026,7 @@ static ant_value_t esm_load_module(ant_t *js, esm_module_t *mod) { return esm_complete_value_module(mod, img_val); } case ESM_MODULE_KIND_NATIVE: { - ant_value_t ns = js_mkobj(js); + ant_value_t ns = esm_make_namespace_object(js); mod->namespace_obj = ns; ant_value_t native_exports = napi_load_native_module(js, mod->resolved_path, ns); @@ -1098,7 +1104,7 @@ static ant_value_t esm_load_module(ant_t *js, esm_module_t *mod) { }} char *js_code = content; - ant_value_t ns = js_mkobj(js); + ant_value_t ns = esm_make_namespace_object(js); mod->namespace_obj = ns; const char *prev_filename = js->filename; diff --git a/src/main.c b/src/main.c index e399a59..2ac99b9 100644 --- a/src/main.c +++ b/src/main.c @@ -81,6 +81,7 @@ #include "modules/globals.h" #include "modules/wasm.h" #include "modules/string_decoder.h" +#include "modules/stream.h" #include "modules/structured-clone.h" #include "modules/v8.h" #include "modules/worker_threads.h" @@ -631,6 +632,8 @@ int main(int argc, char *argv[]) { ant_register_library(internal_http_metadata_library, "ant:internal/http_metadata", NULL); ant_standard_library("util", util_library); + ant_standard_library("util/types", util_types_library); + ant_standard_library("console", console_library); ant_standard_library("net", net_library); ant_standard_library("dns", dns_library); ant_standard_library("assert", assert_library); @@ -652,10 +655,13 @@ int main(int argc, char *argv[]) { ant_standard_library("v8", v8_library); ant_standard_library("zlib", zlib_library); ant_standard_library("string_decoder", string_decoder_library); + ant_standard_library("stream", stream_library); + ant_standard_library("timers", timers_library); ant_standard_library("fs/promises", fs_promises_library); ant_standard_library("timers/promises", timers_promises_library); ant_standard_library("readline/promises", readline_promises_library); + ant_standard_library("stream/promises", stream_promises_library); ant_value_t snapshot_result = ant_load_snapshot(js); if (vtype(snapshot_result) == T_ERR) { diff --git a/src/modules/fs.c b/src/modules/fs.c index 913b058..d88f294 100644 --- a/src/modules/fs.c +++ b/src/modules/fs.c @@ -30,6 +30,7 @@ #include "modules/date.h" #include "modules/buffer.h" #include "modules/events.h" +#include "modules/stream.h" #include "modules/symbol.h" #define fs_err_code(js, code, op, path) ({ \ @@ -134,9 +135,19 @@ typedef struct { ant_value_t listener; } fs_watchfile_options_t; -static ant_value_t g_dirent_proto = 0; -static ant_value_t g_fswatcher_proto = 0; -static ant_value_t g_fswatcher_ctor = 0; +typedef struct { + mode_t mode; + double size, uid, gid; + double atime_ms, mtime_ms, ctime_ms, birthtime_ms; +} fs_stat_fields_t; + +static ant_value_t g_dirent_proto = 0; +static ant_value_t g_fswatcher_proto = 0; +static ant_value_t g_fswatcher_ctor = 0; +static ant_value_t g_readstream_proto = 0; +static ant_value_t g_readstream_ctor = 0; +static ant_value_t g_writestream_proto = 0; +static ant_value_t g_writestream_ctor = 0; static fs_watcher_t *active_watchers = NULL; static UT_array *pending_requests = NULL; @@ -187,11 +198,393 @@ static ant_value_t fs_call_value( return result; } -typedef struct { - mode_t mode; - double size, uid, gid; - double atime_ms, mtime_ms, ctime_ms, birthtime_ms; -} fs_stat_fields_t; +static int parse_open_flags(ant_t *js, ant_value_t arg); +static ant_value_t fs_coerce_path(ant_t *js, ant_value_t arg); + +static ant_value_t fs_stream_error(ant_t *js, ant_value_t stream_obj, const char *op, int uv_code) { + ant_value_t props = js_mkobj(js); + ant_value_t path_val = js_get(js, stream_obj, "path"); + const char *code = uv_err_name(uv_code); + + if (code) js_set(js, props, "code", js_mkstr(js, code, strlen(code))); + js_set(js, props, "errno", js_mknum((double)uv_code)); + if (vtype(path_val) == T_STR) js_set(js, props, "path", path_val); + return js_mkerr_props(js, JS_ERR_TYPE, props, "%s failed: %s", op, uv_strerror(uv_code)); +} + +static ant_value_t fs_stream_push_chunk(ant_t *js, ant_value_t stream_obj, ant_value_t chunk) { + return stream_readable_push(js, stream_obj, chunk, js_mkundef()); +} + +static ant_value_t fs_stream_callback(ant_t *js, ant_value_t callback, ant_value_t value) { + if (!is_callable(callback)) return js_mkundef(); + return fs_call_value(js, callback, js_mkundef(), &value, 1); +} + +static int fs_stream_close_fd_sync(ant_t *js, ant_value_t stream_obj) { + ant_value_t fd_val = js_get(js, stream_obj, "fd"); + uv_fs_t req; + int result = 0; + + if (vtype(fd_val) != T_NUM) { + js_set(js, stream_obj, "pending", js_false); + js_set(js, stream_obj, "closed", js_true); + return 0; + } + + result = uv_fs_close(uv_default_loop(), &req, (uv_file)js_getnum(fd_val), NULL); + uv_fs_req_cleanup(&req); + + js_set(js, stream_obj, "fd", js_mknull()); + js_set(js, stream_obj, "pending", js_false); + js_set(js, stream_obj, "closed", js_true); + + return result; +} + +static int fs_stream_open_fd_sync(ant_t *js, ant_value_t stream_obj) { + ant_value_t fd_val = js_get(js, stream_obj, "fd"); + ant_value_t path_val = js_get(js, stream_obj, "path"); + ant_value_t mode_val = js_get(js, stream_obj, "mode"); + ant_value_t flags_val = js_get_slot(stream_obj, SLOT_FS_FLAGS); + + size_t path_len = 0; + const char *path = NULL; + char *path_copy = NULL; + uv_fs_t req; + + int flags = 0; + int mode = 0666; + int result = 0; + + if (vtype(fd_val) == T_NUM) return (int)js_getnum(fd_val); + if (vtype(path_val) != T_STR) return UV_EINVAL; + + path = js_getstr(js, path_val, &path_len); + if (!path) return UV_EINVAL; + + path_copy = strndup(path, path_len); + if (!path_copy) return UV_ENOMEM; + + flags = (vtype(flags_val) == T_NUM) ? (int)js_getnum(flags_val) : O_RDONLY; + if (vtype(mode_val) == T_NUM) mode = (int)js_getnum(mode_val); + + result = uv_fs_open(uv_default_loop(), &req, path_copy, flags, mode, NULL); + uv_fs_req_cleanup(&req); + free(path_copy); + + if (result < 0) return result; + + js_set(js, stream_obj, "fd", js_mknum((double)result)); + js_set(js, stream_obj, "pending", js_false); + js_set(js, stream_obj, "closed", js_false); + + ant_value_t open_arg = js_mknum((double)result); + eventemitter_emit_args(js, stream_obj, "open", &open_arg, 1); + eventemitter_emit_args(js, stream_obj, "ready", NULL, 0); + + return result; +} + +static ant_value_t fs_stream_destroy(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = js_getthis(js); + ant_value_t err = nargs > 0 ? args[0] : js_mknull(); + ant_value_t callback = nargs > 1 ? args[1] : js_mkundef(); + int result = fs_stream_close_fd_sync(js, stream_obj); + + if (result < 0 && (is_null(err) || is_undefined(err))) + err = fs_stream_error(js, stream_obj, "close", result); + if (is_undefined(err)) err = js_mknull(); + + return fs_stream_callback(js, callback, err); +} + +static ant_value_t fs_stream_close(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = js_getthis(js); + + if (nargs > 0 && is_callable(args[0])) { + eventemitter_add_listener(js, stream_obj, "close", args[0], true); + if (js_truthy(js, js_get(js, stream_obj, "closed"))) + fs_call_value(js, args[0], js_mkundef(), NULL, 0); + } + + if (!js_truthy(js, js_get(js, stream_obj, "destroyed"))) { + ant_value_t destroy_fn = js_getprop_fallback(js, stream_obj, "destroy"); + if (is_callable(destroy_fn)) fs_call_value(js, destroy_fn, stream_obj, NULL, 0); + } + + return stream_obj; +} + +static ant_value_t fs_readstream__read(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = js_getthis(js); + ant_value_t pos_val = js_get(js, stream_obj, "pos"); + ant_value_t end_val = js_get(js, stream_obj, "end"); + ant_value_t bytes_read_val = js_get(js, stream_obj, "bytesRead"); + + int fd = fs_stream_open_fd_sync(js, stream_obj); + int64_t pos = (vtype(pos_val) == T_NUM) ? (int64_t)js_getnum(pos_val) : 0; + int64_t end = (vtype(end_val) == T_NUM) ? (int64_t)js_getnum(end_val) : -1; + + size_t want = 16384; + bool reached_eof = false; + + if (fd < 0) { + ant_value_t err = fs_stream_error(js, stream_obj, "open", fd); + ant_value_t destroy_fn = js_getprop_fallback(js, stream_obj, "destroy"); + if (is_callable(destroy_fn)) fs_call_value(js, destroy_fn, stream_obj, &err, 1); + return js_mkundef(); + } + + if (nargs > 0 && vtype(args[0]) == T_NUM && js_getnum(args[0]) > 0) + want = (size_t)js_getnum(args[0]); + + if (end >= 0) { + if (pos > end) { + if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) fs_stream_close_fd_sync(js, stream_obj); + return fs_stream_push_chunk(js, stream_obj, js_mknull()); + } + if ((int64_t)want > (end - pos + 1)) want = (size_t)(end - pos + 1); + } + + ArrayBufferData *ab = create_array_buffer_data(want); + if (!ab) { + ant_value_t err = js_mkerr(js, "Failed to allocate ReadStream buffer"); + ant_value_t destroy_fn = js_getprop_fallback(js, stream_obj, "destroy"); + if (is_callable(destroy_fn)) fs_call_value(js, destroy_fn, stream_obj, &err, 1); + return js_mkundef(); + } + + uv_fs_t req; + uv_buf_t buf = uv_buf_init((char *)ab->data, (unsigned int)want); + int result = uv_fs_read(uv_default_loop(), &req, fd, &buf, 1, pos, NULL); + uv_fs_req_cleanup(&req); + + if (result < 0) { + ant_value_t err = fs_stream_error(js, stream_obj, "read", result); + ant_value_t destroy_fn = js_getprop_fallback(js, stream_obj, "destroy"); + free_array_buffer_data(ab); + if (is_callable(destroy_fn)) fs_call_value(js, destroy_fn, stream_obj, &err, 1); + return js_mkundef(); + } + + if (result == 0) { + free_array_buffer_data(ab); + if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) + (void)fs_stream_close_fd_sync(js, stream_obj); + return fs_stream_push_chunk(js, stream_obj, js_mknull()); + } + + ant_value_t chunk = create_typed_array(js, TYPED_ARRAY_UINT8, ab, 0, (size_t)result, "Buffer"); + if (vtype(chunk) == T_ERR) { + free_array_buffer_data(ab); + return chunk; + } + + if (result < (int)want) reached_eof = true; + if (end >= 0 && (pos + result - 1) >= end) reached_eof = true; + + js_set(js, stream_obj, "pos", js_mknum((double)(pos + result))); + js_set(js, stream_obj, "bytesRead", js_mknum( + (vtype(bytes_read_val) == T_NUM + ? js_getnum(bytes_read_val) : 0.0) + (double)result + )); + + fs_stream_push_chunk(js, stream_obj, chunk); + + if (reached_eof) { + if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) fs_stream_close_fd_sync(js, stream_obj); + return fs_stream_push_chunk(js, stream_obj, js_mknull()); + } + + return js_mkundef(); +} + +static ant_value_t fs_writestream__write(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = js_getthis(js); + ant_value_t callback = nargs > 2 ? args[2] : js_mkundef(); + ant_value_t pos_val = js_get(js, stream_obj, "pos"); + ant_value_t bytes_written_val = js_get(js, stream_obj, "bytesWritten"); + + const uint8_t *bytes = NULL; + size_t len = 0; + + int fd = fs_stream_open_fd_sync(js, stream_obj); + int64_t pos = (vtype(pos_val) == T_NUM) ? (int64_t)js_getnum(pos_val) : -1; + size_t offset = 0; + + if (fd < 0) return fs_stream_callback(js, callback, fs_stream_error(js, stream_obj, "open", fd)); + + if (vtype(args[0]) == T_STR) { + bytes = (const uint8_t *)js_getstr(js, args[0], &len); + } else if (!buffer_source_get_bytes(js, args[0], &bytes, &len)) { + return fs_stream_callback(js, callback, js_mkerr(js, "WriteStream chunk must be a string or ArrayBufferView")); + } + + while (offset < len) { + uv_fs_t req; + uv_buf_t buf = uv_buf_init((char *)(bytes + offset), (unsigned int)(len - offset)); + int result = uv_fs_write(uv_default_loop(), &req, fd, &buf, 1, pos, NULL); + uv_fs_req_cleanup(&req); + + if (result < 0) { + if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) fs_stream_close_fd_sync(js, stream_obj); + return fs_stream_callback(js, callback, fs_stream_error(js, stream_obj, "write", result)); + } + if (result == 0) { + if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) fs_stream_close_fd_sync(js, stream_obj); + return fs_stream_callback(js, callback, js_mkerr(js, "write failed: short write")); + } + + offset += (size_t)result; + if (pos >= 0) pos += result; + } + + if (pos >= 0) js_set(js, stream_obj, "pos", js_mknum((double)pos)); + js_set(js, stream_obj, "bytesWritten", js_mknum( + (vtype(bytes_written_val) == T_NUM + ? js_getnum(bytes_written_val) : 0.0) + (double)offset + )); + + return fs_stream_callback(js, callback, js_mknull()); +} + +static ant_value_t fs_writestream__final(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = js_getthis(js); + ant_value_t callback = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t value = js_mknull(); + + if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) { + int result = fs_stream_close_fd_sync(js, stream_obj); + if (result < 0) value = fs_stream_error(js, stream_obj, "close", result); + } + + return fs_stream_callback(js, callback, value); +} + +static ant_value_t fs_create_readstream_impl(ant_t *js, ant_value_t path_arg, ant_value_t options_arg, ant_value_t proto) { + ant_value_t path_val = fs_coerce_path(js, path_arg); + ant_value_t options = is_object_type(options_arg) ? options_arg : js_mkobj(js); + ant_value_t stream_options = js_mkobj(js); + + ant_value_t hwm = js_get(js, options, "highWaterMark"); + ant_value_t flags_raw = js_get(js, options, "flags"); + ant_value_t fd_val = js_get(js, options, "fd"); + ant_value_t start_val = js_get(js, options, "start"); + ant_value_t end_val = js_get(js, options, "end"); + ant_value_t mode_val = js_get(js, options, "mode"); + + ant_value_t auto_close_val = js_get(js, options, "autoClose"); + ant_value_t emit_close_val = js_get(js, options, "emitClose"); + ant_value_t stream_obj = 0; + + int flags = parse_open_flags(js, is_undefined(flags_raw) ? js_mkstr(js, "r", 1) : flags_raw); + if (vtype(path_val) != T_STR) return js_mkerr(js, "ReadStream path must be a string"); + if (vtype(hwm) == T_NUM && js_getnum(hwm) > 0) js_set(js, stream_options, "highWaterMark", hwm); + + stream_obj = stream_construct_readable(js, proto, stream_options); + if (is_err(stream_obj)) return stream_obj; + + js_set(js, stream_obj, "_read", js_mkfun(fs_readstream__read)); + js_set(js, stream_obj, "_destroy", js_mkfun(fs_stream_destroy)); + js_set(js, stream_obj, "path", path_val); + js_set(js, stream_obj, "flags", is_undefined(flags_raw) ? js_mkstr(js, "r", 1) : flags_raw); + js_set(js, stream_obj, "mode", vtype(mode_val) == T_NUM ? mode_val : js_mknum(0666)); + js_set(js, stream_obj, "fd", vtype(fd_val) == T_NUM ? fd_val : js_mkundef()); + js_set(js, stream_obj, "pending", js_bool(vtype(fd_val) != T_NUM)); + js_set(js, stream_obj, "closed", js_false); + js_set(js, stream_obj, "autoClose", is_undefined(auto_close_val) ? js_true : js_bool(js_truthy(js, auto_close_val))); + js_set(js, stream_obj, "emitClose", is_undefined(emit_close_val) ? js_true : js_bool(js_truthy(js, emit_close_val))); + js_set(js, stream_obj, "bytesRead", js_mknum(0)); + js_set(js, stream_obj, "start", vtype(start_val) == T_NUM ? start_val : js_mkundef()); + js_set(js, stream_obj, "end", vtype(end_val) == T_NUM ? end_val : js_mkundef()); + js_set(js, stream_obj, "pos", js_mknum(vtype(start_val) == T_NUM ? js_getnum(start_val) : 0.0)); + js_set_slot(stream_obj, SLOT_FS_FLAGS, js_mknum((double)flags)); + + return stream_obj; +} + +static ant_value_t fs_create_writestream_impl(ant_t *js, ant_value_t path_arg, ant_value_t options_arg, ant_value_t proto) { + ant_value_t path_val = fs_coerce_path(js, path_arg); + ant_value_t options = is_object_type(options_arg) ? options_arg : js_mkobj(js); + ant_value_t stream_options = js_mkobj(js); + + ant_value_t flags_raw = js_get(js, options, "flags"); + ant_value_t fd_val = js_get(js, options, "fd"); + ant_value_t start_val = js_get(js, options, "start"); + ant_value_t mode_val = js_get(js, options, "mode"); + ant_value_t auto_close_val = js_get(js, options, "autoClose"); + ant_value_t emit_close_val = js_get(js, options, "emitClose"); + ant_value_t hwm = js_get(js, options, "highWaterMark"); + ant_value_t stream_obj = 0; + + int flags = parse_open_flags(js, is_undefined(flags_raw) ? js_mkstr(js, "w", 1) : flags_raw); + double start_pos = (vtype(start_val) == T_NUM) ? js_getnum(start_val) : ((flags & O_APPEND) ? -1.0 : 0.0); + + if (vtype(path_val) != T_STR) return js_mkerr(js, "WriteStream path must be a string"); + if (vtype(hwm) == T_NUM && js_getnum(hwm) > 0) js_set(js, stream_options, "highWaterMark", hwm); + + stream_obj = stream_construct_writable(js, proto, stream_options); + if (is_err(stream_obj)) return stream_obj; + + js_set(js, stream_obj, "_write", js_mkfun(fs_writestream__write)); + js_set(js, stream_obj, "_final", js_mkfun(fs_writestream__final)); + js_set(js, stream_obj, "_destroy", js_mkfun(fs_stream_destroy)); + js_set(js, stream_obj, "path", path_val); + js_set(js, stream_obj, "flags", is_undefined(flags_raw) ? js_mkstr(js, "w", 1) : flags_raw); + js_set(js, stream_obj, "mode", vtype(mode_val) == T_NUM ? mode_val : js_mknum(0666)); + js_set(js, stream_obj, "fd", vtype(fd_val) == T_NUM ? fd_val : js_mkundef()); + js_set(js, stream_obj, "pending", js_bool(vtype(fd_val) != T_NUM)); + js_set(js, stream_obj, "closed", js_false); + js_set(js, stream_obj, "autoClose", is_undefined(auto_close_val) ? js_true : js_bool(js_truthy(js, auto_close_val))); + js_set(js, stream_obj, "emitClose", is_undefined(emit_close_val) ? js_true : js_bool(js_truthy(js, emit_close_val))); + js_set(js, stream_obj, "bytesWritten", js_mknum(0)); + js_set(js, stream_obj, "start", vtype(start_val) == T_NUM ? start_val : js_mkundef()); + js_set(js, stream_obj, "pos", js_mknum(start_pos)); + js_set_slot(stream_obj, SLOT_FS_FLAGS, js_mknum((double)flags)); + + return stream_obj; +} + +static ant_value_t js_readstream_ctor(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "ReadStream() requires a path argument"); + return fs_create_readstream_impl(js, args[0], nargs > 1 ? args[1] : js_mkundef(), g_readstream_proto); +} + +static ant_value_t js_writestream_ctor(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "WriteStream() requires a path argument"); + return fs_create_writestream_impl(js, args[0], nargs > 1 ? args[1] : js_mkundef(), g_writestream_proto); +} + +static ant_value_t builtin_fs_createReadStream(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "createReadStream() requires a path argument"); + return fs_create_readstream_impl(js, args[0], nargs > 1 ? args[1] : js_mkundef(), g_readstream_proto); +} + +static ant_value_t builtin_fs_createWriteStream(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_mkerr(js, "createWriteStream() requires a path argument"); + return fs_create_writestream_impl(js, args[0], nargs > 1 ? args[1] : js_mkundef(), g_writestream_proto); +} + +static void fs_init_stream_constructors(ant_t *js) { + if (g_readstream_ctor && g_writestream_ctor) return; + + stream_init_constructors(js); + + g_readstream_proto = js_mkobj(js); + js_set_proto_init(g_readstream_proto, stream_readable_prototype(js)); + js_set(js, g_readstream_proto, "close", js_mkfun(fs_stream_close)); + js_set_sym(js, g_readstream_proto, get_toStringTag_sym(), js_mkstr(js, "ReadStream", 10)); + g_readstream_ctor = js_make_ctor(js, js_readstream_ctor, g_readstream_proto, "ReadStream", 10); + js_set_proto_init(g_readstream_ctor, stream_readable_constructor(js)); + + g_writestream_proto = js_mkobj(js); + js_set_proto_init(g_writestream_proto, stream_writable_prototype(js)); + js_set(js, g_writestream_proto, "close", js_mkfun(fs_stream_close)); + js_set_sym(js, g_writestream_proto, get_toStringTag_sym(), js_mkstr(js, "WriteStream", 11)); + g_writestream_ctor = js_make_ctor(js, js_writestream_ctor, g_writestream_proto, "WriteStream", 11); + js_set_proto_init(g_writestream_ctor, stream_writable_constructor(js)); +} static ant_value_t fs_make_date(ant_t *js, double ms) { ant_value_t obj = js_mkobj(js); @@ -3394,11 +3787,14 @@ ant_value_t fs_library(ant_t *js) { fs_set_callback_compatible_methods(js, lib); fs_init_watch_constructors(js); + fs_init_stream_constructors(js); js_set(js, lib, "read", js_mkfun(builtin_fs_read_fd)); js_set(js, lib, "readFileSync", js_mkfun(builtin_fs_readFileSync)); js_set(js, lib, "readSync", js_mkfun(builtin_fs_readSync)); js_set(js, lib, "stream", js_mkfun(builtin_fs_readBytes)); + js_set(js, lib, "createReadStream", js_mkfun(builtin_fs_createReadStream)); + js_set(js, lib, "createWriteStream", js_mkfun(builtin_fs_createWriteStream)); js_set(js, lib, "openSync", js_mkfun(builtin_fs_openSync)); js_set(js, lib, "closeSync", js_mkfun(builtin_fs_closeSync)); js_set(js, lib, "writeFileSync", js_mkfun(builtin_fs_writeFileSync)); @@ -3424,6 +3820,8 @@ ant_value_t fs_library(ant_t *js) { js_set(js, lib, "watchFile", js_mkfun(builtin_fs_watchFile)); js_set(js, lib, "unwatchFile", js_mkfun(builtin_fs_unwatchFile)); js_set(js, lib, "FSWatcher", g_fswatcher_ctor); + js_set(js, lib, "ReadStream", g_readstream_ctor); + js_set(js, lib, "WriteStream", g_writestream_ctor); js_set(js, realpath_sync, "native", realpath_sync); js_set_getter_desc( @@ -3458,6 +3856,10 @@ void gc_mark_fs(ant_t *js, gc_mark_fn mark) { if (g_fswatcher_proto) mark(js, g_fswatcher_proto); if (g_fswatcher_ctor) mark(js, g_fswatcher_ctor); + if (g_readstream_proto) mark(js, g_readstream_proto); + if (g_readstream_ctor) mark(js, g_readstream_ctor); + if (g_writestream_proto) mark(js, g_writestream_proto); + if (g_writestream_ctor) mark(js, g_writestream_ctor); if (!pending_requests) return; unsigned int len = utarray_len(pending_requests); diff --git a/src/modules/io.c b/src/modules/io.c index 87bcf7f..c9e17ee 100644 --- a/src/modules/io.c +++ b/src/modules/io.c @@ -799,8 +799,7 @@ static ant_value_t js_console_inspect(ant_t *js, ant_value_t *args, int nargs) { return js_mkundef(); } -void init_console_module() { - ant_t *js = rt->js; +ant_value_t console_library(ant_t *js) { ant_value_t console_obj = js_mkobj(js); js_set(js, console_obj, "log", js_mkfun(js_console_log)); @@ -816,5 +815,11 @@ void init_console_module() { js_set(js, console_obj, "inspect", js_mkfun(js_console_inspect)); js_set_sym(js, console_obj, get_toStringTag_sym(), js_mkstr(js, "console", 7)); + return console_obj; +} + +void init_console_module() { + ant_t *js = rt->js; + ant_value_t console_obj = console_library(js); js_set(js, js_glob(js), "console", console_obj); } diff --git a/src/modules/readline.c b/src/modules/readline.c index 5826687..48fa160 100644 --- a/src/modules/readline.c +++ b/src/modules/readline.c @@ -516,7 +516,7 @@ static void process_byte(ant_t *js, rl_interface_t *iface, char c) { break; case 127: case 8: handle_backspace(iface); break; case 4: - if (iface->line_len == 0) { emit_event(js, iface, "close", NULL, 0); iface->closed = true; } + if (iface->line_len == 0) rl_close_interface(js, iface); else handle_delete(iface); break; case 1: iface->line_pos = 0; refresh_line(iface); break; @@ -535,7 +535,7 @@ static void on_stdin_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *bu if (!iface || iface->closed || iface->paused) goto cleanup; if (nread < 0) { - if (nread == UV_EOF) { emit_event(js, iface, "close", NULL, 0); iface->closed = true; } + if (nread == UV_EOF) rl_close_interface(js, iface); goto cleanup; } diff --git a/src/modules/stream.c b/src/modules/stream.c new file mode 100644 index 0000000..07b5468 --- /dev/null +++ b/src/modules/stream.c @@ -0,0 +1,1599 @@ +#include +#include + +#include "ant.h" +#include "ptr.h" +#include "internal.h" +#include "silver/engine.h" +#include "esm/loader.h" + +#include "gc/roots.h" + +#include "modules/assert.h" +#include "modules/events.h" +#include "modules/stream.h" +#include "modules/symbol.h" + +enum { STREAM_NATIVE_TAG = 0x5354524Du }; // STRM + +static ant_value_t g_stream_proto = 0; +static ant_value_t g_stream_ctor = 0; + +static ant_value_t g_readable_proto = 0; +static ant_value_t g_readable_ctor = 0; + +static ant_value_t g_writable_proto = 0; +static ant_value_t g_writable_ctor = 0; + +static ant_value_t g_duplex_proto = 0; +static ant_value_t g_duplex_ctor = 0; + +static ant_value_t g_transform_proto = 0; +static ant_value_t g_transform_ctor = 0; + +static ant_value_t g_passthrough_proto = 0; +static ant_value_t g_passthrough_ctor = 0; + +static ant_value_t stream_noop(ant_t *js, ant_value_t *args, int nargs) { + return js_mkundef(); +} + +static bool stream_is_instance(ant_value_t value) { + return is_object_type(value) && js_check_native_tag(value, STREAM_NATIVE_TAG); +} + +static ant_value_t stream_require_this(ant_t *js, ant_value_t value, const char *label) { + if (!stream_is_instance(value)) + return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid %s", label); + return value; +} + +static ant_value_t stream_truthy_or_object(ant_t *js, ant_value_t value) { + return js_truthy(js, value) ? value : js_mkobj(js); +} + +static bool stream_key_is_cstr(ant_t *js, ant_value_t value, const char *expected) { + size_t len = 0; + const char *s = NULL; + if (vtype(value) != T_STR) return false; + s = js_getstr(js, value, &len); + return s && len == strlen(expected) && memcmp(s, expected, len) == 0; +} + +static ant_value_t stream_event_key(ant_t *js, ant_value_t value) { + uint8_t t = vtype(value); + if (t == T_STR || t == T_SYMBOL) return value; + return js_mkerr(js, "event must be a string or Symbol"); +} + +static ant_value_t stream_call( + ant_t *js, + ant_value_t fn, + ant_value_t this_val, + ant_value_t *args, + int nargs, + bool is_ctor +) { + if (!is_callable(fn)) return js_mkundef(); + return sv_vm_call(js->vm, js, fn, this_val, args, nargs, NULL, is_ctor); +} + +static ant_value_t stream_call_prop( + ant_t *js, + ant_value_t target, + const char *name, + ant_value_t *args, + int nargs +) { + ant_value_t fn = js_getprop_fallback(js, target, name); + if (is_err(fn) || !is_callable(fn)) return js_mkundef(); + return stream_call(js, fn, target, args, nargs, false); +} + +static void stream_call_callback(ant_t *js, ant_value_t fn, ant_value_t *args, int nargs) { + if (!is_callable(fn)) return; + stream_call(js, fn, js_mkundef(), args, nargs, false); +} + +static void stream_schedule_microtask(ant_t *js, ant_cfunc_t fn, ant_value_t data) { + ant_value_t promise = js_mkpromise(js); + ant_value_t cb = js_heavy_mkfun(js, fn, data); + ant_value_t then_result = 0; + + js_resolve_promise(js, promise, js_mkundef()); + then_result = js_promise_then(js, promise, cb, js_mkundef()); + promise_mark_handled(then_result); +} + +static ant_value_t stream_buffer_ctor(ant_t *js) { + ant_value_t ns = js_esm_import_sync_cstr(js, "buffer", 6); + if (is_err(ns)) return ns; + return js_get(js, ns, "Buffer"); +} + +static ant_value_t stream_make_buffer(ant_t *js, ant_value_t value, ant_value_t encoding) { + ant_value_t buffer_ctor = stream_buffer_ctor(js); + ant_value_t from_fn = 0; + ant_value_t args[2]; + + if (is_err(buffer_ctor)) return buffer_ctor; + from_fn = js_get(js, buffer_ctor, "from"); + if (is_err(from_fn) || !is_callable(from_fn)) + return js_mkerr(js, "Buffer.from is not available"); + + args[0] = value; + args[1] = encoding; + return stream_call(js, from_fn, buffer_ctor, args, 2, false); +} + +static ant_value_t stream_normalize_chunk( + ant_t *js, + ant_value_t chunk, + bool object_mode, + ant_value_t encoding +) { + ant_value_t str_val = 0; + + if (object_mode || is_null(chunk) || is_undefined(chunk) || vtype(chunk) == T_TYPEDARRAY) + return chunk; + + if (vtype(chunk) == T_STR) return stream_make_buffer(js, chunk, encoding); + + str_val = js_tostring_val(js, chunk); + if (is_err(str_val)) return str_val; + return stream_make_buffer(js, str_val, encoding); +} + +static ant_value_t stream_readable_state(ant_t *js, ant_value_t stream_obj) { + return js_get(js, stream_obj, "_readableState"); +} + +static ant_value_t stream_writable_state(ant_t *js, ant_value_t stream_obj) { + return js_get(js, stream_obj, "_writableState"); +} + +static ant_value_t stream_pipes(ant_t *js, ant_value_t stream_obj) { + return js_get(js, stream_obj, "_pipes"); +} + +static ant_value_t stream_readable_buffer(ant_t *js, ant_value_t stream_obj) { + ant_value_t state = stream_readable_state(js, stream_obj); + if (!is_object_type(state)) return js_mkundef(); + return js_get(js, state, "buffer"); +} + +static ant_offset_t stream_readable_buffer_head(ant_t *js, ant_value_t stream_obj) { + ant_value_t state = stream_readable_state(js, stream_obj); + ant_value_t head = is_object_type(state) ? js_get(js, state, "bufferHead") : js_mkundef(); + return vtype(head) == T_NUM ? (ant_offset_t)js_getnum(head) : 0; +} + +static void stream_set_readable_buffer_head(ant_t *js, ant_value_t stream_obj, ant_offset_t head) { + ant_value_t state = stream_readable_state(js, stream_obj); + if (is_object_type(state)) js_set(js, state, "bufferHead", js_mknum((double)head)); +} + +static ant_offset_t stream_readable_buffer_len(ant_t *js, ant_value_t stream_obj) { + ant_value_t buffer = stream_readable_buffer(js, stream_obj); + ant_offset_t head = stream_readable_buffer_head(js, stream_obj); + ant_offset_t len = vtype(buffer) == T_ARR ? js_arr_len(js, buffer) : 0; + return len > head ? len - head : 0; +} + +static void stream_compact_readable_buffer(ant_t *js, ant_value_t stream_obj) { + ant_value_t state = stream_readable_state(js, stream_obj); + ant_value_t buffer = stream_readable_buffer(js, stream_obj); + ant_offset_t head = stream_readable_buffer_head(js, stream_obj); + ant_offset_t len = vtype(buffer) == T_ARR ? js_arr_len(js, buffer) : 0; + ant_value_t compact = 0; + + if (!is_object_type(state) || vtype(buffer) != T_ARR) return; + if (head == 0) return; + + if (head >= len) { + compact = js_mkarr(js); + js_set(js, state, "buffer", compact); + js_set(js, state, "bufferHead", js_mknum(0)); + return; + } + + if (head <= 32 && head * 2 < len) return; + compact = js_mkarr(js); + for (ant_offset_t i = head; i < len; i++) js_arr_push(js, compact, js_arr_get(js, buffer, i)); + + js_set(js, state, "buffer", compact); + js_set(js, state, "bufferHead", js_mknum(0)); +} + +static void stream_buffer_push(ant_t *js, ant_value_t stream_obj, ant_value_t value) { + ant_value_t state = stream_readable_state(js, stream_obj); + ant_value_t buffer = stream_readable_buffer(js, stream_obj); + + if (!is_object_type(state) || vtype(buffer) != T_ARR) return; + js_arr_push(js, buffer, value); +} + +static ant_value_t stream_buffer_shift(ant_t *js, ant_value_t stream_obj) { + ant_value_t buffer = stream_readable_buffer(js, stream_obj); + ant_offset_t head = stream_readable_buffer_head(js, stream_obj); + ant_offset_t len = vtype(buffer) == T_ARR ? js_arr_len(js, buffer) : 0; + ant_value_t value = js_mkundef(); + + if (vtype(buffer) != T_ARR || head >= len) return js_mkundef(); + value = js_arr_get(js, buffer, head); + stream_set_readable_buffer_head(js, stream_obj, head + 1); + stream_compact_readable_buffer(js, stream_obj); + return value; +} + +static bool stream_listener_count_positive(ant_t *js, ant_value_t target, const char *event_name) { + ant_value_t args[1]; + ant_value_t result = 0; + + args[0] = js_mkstr(js, event_name, strlen(event_name)); + result = stream_call_prop(js, target, "listenerCount", args, 1); + return vtype(result) == T_NUM && js_getnum(result) > 0; +} + +static void stream_remove_listener( + ant_t *js, + ant_value_t target, + const char *event_name, + ant_value_t listener +) { + ant_value_t args[2]; + args[0] = js_mkstr(js, event_name, strlen(event_name)); + args[1] = listener; + stream_call_prop(js, target, "removeListener", args, 2); +} + +static ant_value_t stream_get_option(ant_t *js, ant_value_t options, const char *name) { + if (!is_object_type(options)) return js_mkundef(); + return js_get(js, options, name); +} + +static ant_value_t stream_make_base_object(ant_t *js, ant_value_t proto) { + ant_value_t obj = js_mkobj(js); + if (is_object_type(proto)) js_set_proto_init(obj, proto); + js_set_native_tag(obj, STREAM_NATIVE_TAG); + return obj; +} + +static void stream_init_base(ant_t *js, ant_value_t obj, ant_value_t raw_options) { + ant_value_t pipes = js_mkarr(js); + js_set(js, obj, "readable", js_true); + js_set(js, obj, "writable", js_true); + js_set(js, obj, "destroyed", js_false); + js_set(js, obj, "_paused", js_false); + js_set(js, obj, "_pipes", pipes); + js_set(js, obj, "_streamOptions", stream_truthy_or_object(js, raw_options)); +} + +static void stream_init_readable(ant_t *js, ant_value_t obj, ant_value_t raw_options) { + ant_value_t options = is_object_type(raw_options) ? raw_options : js_mkobj(js); + ant_value_t state = js_mkobj(js); + ant_value_t read_fn = stream_get_option(js, options, "read"); + ant_value_t hwm = stream_get_option(js, options, "highWaterMark"); + double high_water_mark = (vtype(hwm) == T_NUM && js_getnum(hwm) > 0) ? js_getnum(hwm) : 16384.0; + + stream_init_base(js, obj, raw_options); + js_set(js, obj, "readable", js_true); + js_set(js, obj, "writable", js_false); + js_set(js, obj, "readableEnded", js_false); + + js_set(js, state, "objectMode", js_bool(js_truthy(js, stream_get_option(js, options, "objectMode")))); + js_set(js, state, "ended", js_false); + js_set(js, state, "endEmitted", js_false); + js_set(js, state, "flowing", js_false); + js_set(js, state, "reading", js_false); + js_set(js, state, "highWaterMark", js_mknum(high_water_mark)); + js_set(js, state, "buffer", js_mkarr(js)); + js_set(js, state, "bufferHead", js_mknum(0)); + js_set(js, obj, "_readableState", state); + + if (is_callable(read_fn)) js_set(js, obj, "_read", read_fn); +} + +static void stream_init_writable(ant_t *js, ant_value_t obj, ant_value_t raw_options) { + ant_value_t options = is_object_type(raw_options) ? raw_options : js_mkobj(js); + ant_value_t state = js_mkobj(js); + bool object_mode = js_truthy(js, stream_get_option(js, options, "objectMode")) + || js_truthy(js, stream_get_option(js, options, "writableObjectMode")); + ant_value_t write_fn = stream_get_option(js, options, "write"); + ant_value_t final_fn = stream_get_option(js, options, "final"); + ant_value_t destroy_fn = stream_get_option(js, options, "destroy"); + + stream_init_base(js, obj, raw_options); + js_set(js, obj, "readable", js_false); + js_set(js, obj, "writable", js_true); + js_set(js, obj, "writableEnded", js_false); + js_set(js, obj, "writableFinished", js_false); + + js_set(js, state, "objectMode", js_bool(object_mode)); + js_set(js, state, "finished", js_false); + js_set(js, state, "ended", js_false); + js_set(js, obj, "_writableState", state); + + if (is_callable(write_fn)) js_set(js, obj, "_write", write_fn); + if (is_callable(final_fn)) js_set(js, obj, "_final", final_fn); + if (is_callable(destroy_fn)) js_set(js, obj, "_destroy", destroy_fn); +} + +static ant_value_t stream_construct( + ant_t *js, + ant_value_t base_proto, + ant_value_t raw_options, + void (*init_fn)(ant_t *, ant_value_t, ant_value_t) +) { + ant_value_t proto = js_instance_proto_from_new_target(js, base_proto); + ant_value_t obj = stream_make_base_object(js, is_object_type(proto) ? proto : base_proto); + init_fn(js, obj, raw_options); + return obj; +} + +static ant_value_t stream_emit_named(ant_t *js, ant_value_t stream_obj, const char *event_name) { + return js_bool(eventemitter_emit_args(js, stream_obj, event_name, NULL, 0)); +} + +static void stream_emit_error(ant_t *js, ant_value_t stream_obj, ant_value_t error) { + ant_value_t args[1]; + args[0] = error; + eventemitter_emit_args(js, stream_obj, "error", args, 1); +} + +static ant_value_t stream_readable_maybe_read(ant_t *js, ant_value_t stream_obj); +static ant_value_t stream_readable_flush(ant_t *js, ant_value_t stream_obj); +static ant_value_t stream_readable_push_value(ant_t *js, ant_value_t stream_obj, ant_value_t chunk, ant_value_t encoding); + +static ant_value_t js_stream_pause(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "stream"); + if (is_err(stream_obj)) return stream_obj; + + js_set(js, stream_obj, "_paused", js_true); + stream_emit_named(js, stream_obj, "pause"); + return stream_obj; +} + +static ant_value_t js_stream_resume(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "stream"); + if (is_err(stream_obj)) return stream_obj; + + js_set(js, stream_obj, "_paused", js_false); + stream_emit_named(js, stream_obj, "resume"); + return stream_obj; +} + +static ant_value_t js_stream_is_paused(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "stream"); + ant_value_t paused = 0; + if (is_err(stream_obj)) return stream_obj; + paused = js_get(js, stream_obj, "_paused"); + return js_bool(js_truthy(js, paused)); +} + +static void stream_pipe_remove_state(ant_t *js, ant_value_t source, ant_value_t state_obj) { + ant_value_t pipes = stream_pipes(js, source); + ant_offset_t len = vtype(pipes) == T_ARR ? js_arr_len(js, pipes) : 0; + ant_value_t next = js_mkarr(js); + + for (ant_offset_t i = 0; i < len; i++) { + ant_value_t item = js_arr_get(js, pipes, i); + if (item != state_obj) js_arr_push(js, next, item); + } + + js_set(js, source, "_pipes", next); +} + +static void stream_pipe_cleanup(ant_t *js, ant_value_t state_obj) { + ant_value_t cleaned = js_get(js, state_obj, "cleaned"); + ant_value_t source = js_get(js, state_obj, "source"); + ant_value_t dest = js_get(js, state_obj, "dest"); + ant_value_t on_data = js_get(js, state_obj, "onData"); + ant_value_t on_drain = js_get(js, state_obj, "onDrain"); + ant_value_t on_end = js_get(js, state_obj, "onEnd"); + ant_value_t on_close = js_get(js, state_obj, "onClose"); + ant_value_t on_error = js_get(js, state_obj, "onError"); + + if (js_truthy(js, cleaned)) return; + js_set(js, state_obj, "cleaned", js_true); + + if (stream_is_instance(source)) { + stream_remove_listener(js, source, "data", on_data); + stream_remove_listener(js, source, "end", on_end); + stream_remove_listener(js, source, "close", on_close); + stream_remove_listener(js, source, "error", on_error); + stream_pipe_remove_state(js, source, state_obj); + } + + if (is_object_type(dest)) + stream_remove_listener(js, dest, "drain", on_drain); +} + +static ant_value_t stream_pipe_on_data(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t source = js_get(js, state_obj, "source"); + ant_value_t dest = js_get(js, state_obj, "dest"); + ant_value_t result = js_mkundef(); + + if (!is_object_type(dest)) return js_mkundef(); + result = stream_call_prop(js, dest, "write", nargs > 0 ? &args[0] : NULL, nargs > 0 ? 1 : 0); + if (is_err(result)) return result; + + if (result == js_false) stream_call_prop(js, source, "pause", NULL, 0); + return js_mkundef(); +} + +static ant_value_t stream_pipe_on_drain(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t source = js_get(js, state_obj, "source"); + stream_call_prop(js, source, "resume", NULL, 0); + return js_mkundef(); +} + +static ant_value_t stream_pipe_on_end(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t dest = js_get(js, state_obj, "dest"); + bool end_dest = js_truthy(js, js_get(js, state_obj, "end")); + stream_pipe_cleanup(js, state_obj); + if (end_dest) stream_call_prop(js, dest, "end", NULL, 0); + return js_mkundef(); +} + +static ant_value_t stream_pipe_on_close(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + stream_pipe_cleanup(js, state_obj); + return js_mkundef(); +} + +static ant_value_t stream_pipe_on_error(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t dest = js_get(js, state_obj, "dest"); + stream_pipe_cleanup(js, state_obj); + if (is_object_type(dest) && stream_listener_count_positive(js, dest, "error") && nargs > 0) + eventemitter_emit_args(js, dest, "error", &args[0], 1); + return js_mkundef(); +} + +static ant_value_t js_stream_pipe(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t source = stream_require_this(js, js_getthis(js), "stream"); + ant_value_t options = nargs > 1 ? args[1] : js_mkundef(); + ant_value_t state_obj = 0; + ant_value_t readable_state = 0; + bool end_dest = true; + + if (is_err(source)) return source; + if (nargs < 1 || !is_object_type(args[0])) return js_mkerr(js, "pipe requires a destination stream"); + if (is_object_type(options)) { + ant_value_t end_val = js_get(js, options, "end"); + if (!is_undefined(end_val)) end_dest = end_val != js_false; + } + + state_obj = js_mkobj(js); + js_set(js, state_obj, "source", source); + js_set(js, state_obj, "dest", args[0]); + js_set(js, state_obj, "end", js_bool(end_dest)); + js_set(js, state_obj, "cleaned", js_false); + js_set(js, state_obj, "onData", js_heavy_mkfun(js, stream_pipe_on_data, state_obj)); + js_set(js, state_obj, "onDrain", js_heavy_mkfun(js, stream_pipe_on_drain, state_obj)); + js_set(js, state_obj, "onEnd", js_heavy_mkfun(js, stream_pipe_on_end, state_obj)); + js_set(js, state_obj, "onClose", js_heavy_mkfun(js, stream_pipe_on_close, state_obj)); + js_set(js, state_obj, "onError", js_heavy_mkfun(js, stream_pipe_on_error, state_obj)); + + js_arr_push(js, stream_pipes(js, source), state_obj); + eventemitter_add_listener(js, source, "data", js_get(js, state_obj, "onData"), false); + eventemitter_add_listener(js, source, "end", js_get(js, state_obj, "onEnd"), true); + eventemitter_add_listener(js, source, "close", js_get(js, state_obj, "onClose"), true); + eventemitter_add_listener(js, source, "error", js_get(js, state_obj, "onError"), false); + eventemitter_add_listener(js, args[0], "drain", js_get(js, state_obj, "onDrain"), false); + eventemitter_emit_args(js, args[0], "pipe", &source, 1); + readable_state = stream_readable_state(js, source); + if (is_object_type(readable_state)) js_set(js, readable_state, "flowing", js_true); + stream_call_prop(js, source, "resume", NULL, 0); + + return args[0]; +} + +static ant_value_t js_stream_unpipe(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t source = stream_require_this(js, js_getthis(js), "stream"); + ant_value_t pipes = 0; + ant_value_t matches = 0; + ant_offset_t len = 0; + ant_value_t dest = nargs > 0 ? args[0] : js_mkundef(); + + if (is_err(source)) return source; + pipes = stream_pipes(js, source); + if (vtype(pipes) != T_ARR) return source; + + matches = js_mkarr(js); + len = js_arr_len(js, pipes); + for (ant_offset_t i = 0; i < len; i++) { + ant_value_t state_obj = js_arr_get(js, pipes, i); + ant_value_t entry_dest = js_get(js, state_obj, "dest"); + if (!is_object_type(dest) || entry_dest == dest) js_arr_push(js, matches, state_obj); + } + + len = js_arr_len(js, matches); + for (ant_offset_t i = 0; i < len; i++) stream_pipe_cleanup(js, js_arr_get(js, matches, i)); + return source; +} + +static ant_value_t stream_destroy_done(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t stream_obj = js_get(js, state_obj, "stream"); + ant_value_t destroyed_err = (nargs > 0) ? args[0] : js_mkundef(); + if (!is_null(destroyed_err) && !is_undefined(destroyed_err)) stream_emit_error(js, stream_obj, destroyed_err); + stream_emit_named(js, stream_obj, "close"); + return js_mkundef(); +} + +static ant_value_t stream_once_call(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t fn = js_get(js, state_obj, "fn"); + ant_value_t this_val = js_get(js, state_obj, "thisVal"); + ant_value_t called = js_get(js, state_obj, "called"); + + if (js_truthy(js, called)) return js_mkundef(); + js_set(js, state_obj, "called", js_true); + return stream_call(js, fn, this_val, args, nargs, false); +} + +static ant_value_t stream_make_once(ant_t *js, ant_value_t fn, ant_value_t this_val) { + ant_value_t state_obj = js_mkobj(js); + js_set(js, state_obj, "fn", fn); + js_set(js, state_obj, "thisVal", this_val); + js_set(js, state_obj, "called", js_false); + return js_heavy_mkfun(js, stream_once_call, state_obj); +} + +static ant_value_t js_stream_destroy(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "stream"); + ant_value_t destroy_fn = 0; + ant_value_t done_state = 0; + ant_value_t done = 0; + ant_value_t destroy_args[2]; + ant_value_t error = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t result = 0; + + if (is_err(stream_obj)) return stream_obj; + if (js_truthy(js, js_get(js, stream_obj, "destroyed"))) return stream_obj; + + js_set(js, stream_obj, "destroyed", js_true); + + done_state = js_mkobj(js); + js_set(js, done_state, "stream", stream_obj); + done = stream_make_once(js, js_heavy_mkfun(js, stream_destroy_done, done_state), js_mkundef()); + destroy_fn = js_get(js, stream_obj, "_destroy"); + + if (is_callable(destroy_fn)) { + destroy_args[0] = is_undefined(error) ? js_mknull() : error; + destroy_args[1] = done; + result = stream_call(js, destroy_fn, stream_obj, destroy_args, 2, false); + return is_err(result) ? result : stream_obj; + } + + destroy_args[0] = is_undefined(error) ? js_mknull() : error; + stream_call_callback(js, done, destroy_args, 1); + return stream_obj; +} + +static ant_value_t js_readable__read(ant_t *js, ant_value_t *args, int nargs) { + return js_mkundef(); +} + +static ant_value_t stream_readable_start_flowing(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t state = stream_readable_state(js, stream_obj); + + if (!is_object_type(state)) return js_mkundef(); + if (!js_truthy(js, js_get(js, state, "flowing"))) return js_mkundef(); + + { + ant_value_t saved_this = js->this_val; + js->this_val = stream_obj; + js_stream_resume(js, NULL, 0); + js->this_val = saved_this; + } + stream_readable_maybe_read(js, stream_obj); + stream_readable_flush(js, stream_obj); + return js_mkundef(); +} + +static ant_value_t stream_readable_flush(ant_t *js, ant_value_t stream_obj) { + ant_value_t state = stream_readable_state(js, stream_obj); + + if (!is_object_type(state)) return js_mkundef(); + + while (js_truthy(js, js_get(js, state, "flowing")) && stream_readable_buffer_len(js, stream_obj) > 0) { + ant_value_t chunk = stream_buffer_shift(js, stream_obj); + eventemitter_emit_args(js, stream_obj, "data", &chunk, 1); + } + + if ( + js_truthy(js, js_get(js, state, "ended")) && + stream_readable_buffer_len(js, stream_obj) == 0 && + !js_truthy(js, js_get(js, state, "endEmitted")) + ) { + js_set(js, state, "endEmitted", js_true); + js_set(js, stream_obj, "readableEnded", js_true); + stream_emit_named(js, stream_obj, "end"); + stream_emit_named(js, stream_obj, "close"); + } + + return js_mkundef(); +} + +static ant_value_t stream_readable_maybe_read(ant_t *js, ant_value_t stream_obj) { + ant_value_t state = stream_readable_state(js, stream_obj); + ant_value_t read_fn = 0; + ant_value_t args[1]; + + if (!is_object_type(state)) return js_mkundef(); + if (js_truthy(js, js_get(js, stream_obj, "destroyed"))) return js_mkundef(); + if (js_truthy(js, js_get(js, state, "reading"))) return js_mkundef(); + if (js_truthy(js, js_get(js, state, "ended"))) return js_mkundef(); + if (stream_readable_buffer_len(js, stream_obj) > 0) return js_mkundef(); + + read_fn = js_get(js, stream_obj, "_read"); + js_set(js, state, "reading", js_true); + args[0] = js_get(js, state, "highWaterMark"); + if (is_callable(read_fn)) stream_call(js, read_fn, stream_obj, args, 1, false); + js_set(js, state, "reading", js_false); + return js_mkundef(); +} + +static ant_value_t stream_readable_push_value( + ant_t *js, + ant_value_t stream_obj, + ant_value_t chunk, + ant_value_t encoding +) { + ant_value_t state = stream_readable_state(js, stream_obj); + ant_value_t normalized = 0; + + if (!is_object_type(state)) return js_false; + if (js_truthy(js, js_get(js, stream_obj, "destroyed"))) return js_false; + + if (is_null(chunk)) { + js_set(js, state, "ended", js_true); + stream_readable_flush(js, stream_obj); + return js_false; + } + + normalized = stream_normalize_chunk( + js, + chunk, + js_truthy(js, js_get(js, state, "objectMode")), + is_undefined(encoding) ? js_mkstr(js, "utf8", 4) : encoding + ); + if (is_err(normalized)) return normalized; + + stream_buffer_push(js, stream_obj, normalized); + if (js_truthy(js, js_get(js, state, "flowing"))) stream_readable_flush(js, stream_obj); + return js_bool(js_truthy(js, js_get(js, state, "flowing"))); +} + +static ant_value_t js_readable_push(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Readable"); + ant_value_t chunk = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t encoding = nargs > 1 ? args[1] : js_mkundef(); + if (is_err(stream_obj)) return stream_obj; + return stream_readable_push_value(js, stream_obj, chunk, encoding); +} + +static ant_value_t js_readable_read(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Readable"); + ant_value_t state = 0; + ant_value_t chunk = 0; + + if (is_err(stream_obj)) return stream_obj; + state = stream_readable_state(js, stream_obj); + if (!is_object_type(state)) return js_mknull(); + + if (stream_readable_buffer_len(js, stream_obj) == 0) stream_readable_maybe_read(js, stream_obj); + if (stream_readable_buffer_len(js, stream_obj) == 0) return js_mknull(); + + chunk = stream_buffer_shift(js, stream_obj); + if (js_truthy(js, js_get(js, state, "flowing"))) stream_readable_flush(js, stream_obj); + return chunk; +} + +static ant_value_t js_readable_on(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Readable"); + ant_value_t key = 0; + ant_value_t state = 0; + + if (is_err(stream_obj)) return stream_obj; + if (nargs < 2) return js_mkerr(js, "on requires 2 arguments (event, listener)"); + key = stream_event_key(js, args[0]); + if (is_err(key)) return key; + if (!eventemitter_add_listener_val(js, stream_obj, key, args[1], false)) + return js_mkerr(js, "listener must be a function"); + + if (stream_key_is_cstr(js, key, "data")) { + state = stream_readable_state(js, stream_obj); + if (is_object_type(state)) js_set(js, state, "flowing", js_true); + stream_schedule_microtask(js, stream_readable_start_flowing, stream_obj); + } + + return stream_obj; +} + +static ant_value_t js_readable_resume(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Readable"); + ant_value_t state = 0; + if (is_err(stream_obj)) return stream_obj; + + state = stream_readable_state(js, stream_obj); + if (is_object_type(state)) js_set(js, state, "flowing", js_true); + js_stream_resume(js, NULL, 0); + stream_readable_maybe_read(js, stream_obj); + stream_readable_flush(js, stream_obj); + return stream_obj; +} + +static ant_value_t js_readable_pause(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Readable"); + ant_value_t state = 0; + if (is_err(stream_obj)) return stream_obj; + + state = stream_readable_state(js, stream_obj); + if (is_object_type(state)) js_set(js, state, "flowing", js_false); + return js_stream_pause(js, args, nargs); +} + +static ant_value_t js_writable__write(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t callback = nargs > 2 ? args[2] : js_mkundef(); + stream_call_callback(js, callback, NULL, 0); + return js_mkundef(); +} + +static ant_value_t js_writable__final(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t callback = nargs > 0 ? args[0] : js_mkundef(); + stream_call_callback(js, callback, NULL, 0); + return js_mkundef(); +} + +static ant_value_t stream_writable_write_done(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t stream_obj = js_get(js, state_obj, "stream"); + ant_value_t callback = js_get(js, state_obj, "callback"); + ant_value_t err = nargs > 0 ? args[0] : js_mkundef(); + + if (!is_undefined(err) && !is_null(err)) { + ant_value_t destroy_args[1] = { err }; + js_set(js, state_obj, "done", js_true); + { + ant_value_t saved_this = js->this_val; + js->this_val = stream_obj; + js_stream_destroy(js, destroy_args, 1); + js->this_val = saved_this; + } + if (is_callable(callback)) stream_call_callback(js, callback, &err, 1); + return js_mkundef(); + } + + if (is_callable(callback)) stream_call_callback(js, callback, NULL, 0); + stream_emit_named(js, stream_obj, "drain"); + return js_mkundef(); +} + +static ant_value_t js_writable_write(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Writable"); + ant_value_t state = 0; + ant_value_t callback = js_mkundef(); + ant_value_t encoding = js_mkstr(js, "utf8", 4); + ant_value_t normalized = 0; + ant_value_t write_fn = 0; + ant_value_t done_state = 0; + ant_value_t done = 0; + ant_value_t write_args[3]; + + if (is_err(stream_obj)) return stream_obj; + state = stream_writable_state(js, stream_obj); + if (!is_object_type(state)) return js_false; + + if (nargs > 1 && is_callable(args[1])) callback = args[1]; + else if (nargs > 1 && !is_undefined(args[1])) encoding = args[1]; + if (nargs > 2 && is_callable(args[2])) callback = args[2]; + + if (js_truthy(js, js_get(js, stream_obj, "writableEnded")) || js_truthy(js, js_get(js, stream_obj, "destroyed"))) { + ant_value_t err = js_mkerr(js, "write after end"); + if (is_callable(callback)) stream_call_callback(js, callback, &err, 1); + else stream_emit_error(js, stream_obj, err); + return js_false; + } + + normalized = stream_normalize_chunk( + js, + nargs > 0 ? args[0] : js_mkundef(), + js_truthy(js, js_get(js, state, "objectMode")), + encoding + ); + if (is_err(normalized)) return normalized; + + done_state = js_mkobj(js); + js_set(js, done_state, "stream", stream_obj); + js_set(js, done_state, "callback", callback); + js_set(js, done_state, "done", js_false); + done = stream_make_once(js, js_heavy_mkfun(js, stream_writable_write_done, done_state), js_mkundef()); + write_fn = js_get(js, stream_obj, "_write"); + + write_args[0] = normalized; + write_args[1] = encoding; + write_args[2] = done; + if (is_callable(write_fn)) { + ant_value_t result = stream_call(js, write_fn, stream_obj, write_args, 3, false); + if (is_err(result)) { + ant_value_t err_args[1] = { result }; + stream_call_callback(js, done, err_args, 1); + return js_false; + } + } + + return js_bool(!js_truthy(js, js_get(js, stream_obj, "destroyed"))); +} + +static ant_value_t stream_writable_end_done(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t stream_obj = js_get(js, state_obj, "stream"); + ant_value_t callback = js_get(js, state_obj, "callback"); + ant_value_t err = nargs > 0 ? args[0] : js_mkundef(); + + if (!is_undefined(err) && !is_null(err)) { + ant_value_t destroy_args[1] = { err }; + ant_value_t saved_this = js->this_val; + js->this_val = stream_obj; + js_stream_destroy(js, destroy_args, 1); + js->this_val = saved_this; + if (is_callable(callback)) stream_call_callback(js, callback, &err, 1); + return js_mkundef(); + } + + js_set(js, stream_obj, "writableFinished", js_true); + js_set(js, stream_writable_state(js, stream_obj), "finished", js_true); + stream_emit_named(js, stream_obj, "finish"); + if (is_callable(callback)) stream_call_callback(js, callback, NULL, 0); + if (!js_truthy(js, js_get(js, stream_obj, "readable"))) stream_emit_named(js, stream_obj, "close"); + return js_mkundef(); +} + +static ant_value_t js_writable_end(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Writable"); + ant_value_t callback = js_mkundef(); + ant_value_t chunk = js_mkundef(); + ant_value_t encoding = js_mkundef(); + ant_value_t final_fn = 0; + ant_value_t final_args[1]; + ant_value_t done_state = 0; + ant_value_t done = 0; + + if (is_err(stream_obj)) return stream_obj; + + if (nargs > 0 && is_callable(args[0])) callback = args[0]; + else { + if (nargs > 0) chunk = args[0]; + if (nargs > 1 && is_callable(args[1])) callback = args[1]; + else if (nargs > 1) encoding = args[1]; + if (nargs > 2 && is_callable(args[2])) callback = args[2]; + } + + if (!is_undefined(chunk) && !is_null(chunk)) { + ant_value_t write_args[3]; + ant_value_t saved_this = js->this_val; + write_args[0] = chunk; + write_args[1] = encoding; + write_args[2] = js_mkundef(); + js->this_val = stream_obj; + js_writable_write(js, write_args, 3); + js->this_val = saved_this; + } + + if (js_truthy(js, js_get(js, stream_obj, "writableEnded"))) { + if (is_callable(callback)) stream_call_callback(js, callback, NULL, 0); + return stream_obj; + } + + js_set(js, stream_obj, "writableEnded", js_true); + js_set(js, stream_writable_state(js, stream_obj), "ended", js_true); + + done_state = js_mkobj(js); + js_set(js, done_state, "stream", stream_obj); + js_set(js, done_state, "callback", callback); + done = stream_make_once(js, js_heavy_mkfun(js, stream_writable_end_done, done_state), js_mkundef()); + + final_fn = js_get(js, stream_obj, "_final"); + final_args[0] = done; + if (is_callable(final_fn)) stream_call(js, final_fn, stream_obj, final_args, 1, false); + else stream_call_callback(js, done, NULL, 0); + + return stream_obj; +} + +static ant_value_t js_transform__transform(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t callback = nargs > 2 ? args[2] : js_mkundef(); + ant_value_t cb_args[2]; + cb_args[0] = js_mknull(); + cb_args[1] = nargs > 0 ? args[0] : js_mkundef(); + stream_call_callback(js, callback, cb_args, 2); + return js_mkundef(); +} + +static ant_value_t stream_transform_write_callback(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t stream_obj = js_get(js, state_obj, "stream"); + ant_value_t outer_callback = js_get(js, state_obj, "callback"); + + if (nargs > 0 && !is_null(args[0]) && !is_undefined(args[0])) { + stream_call_callback(js, outer_callback, &args[0], 1); + return js_mkundef(); + } + + if (nargs > 1 && !is_null(args[1]) && !is_undefined(args[1])) + stream_readable_push_value(js, stream_obj, args[1], js_mkundef()); + + stream_call_callback(js, outer_callback, NULL, 0); + return js_mkundef(); +} + +static ant_value_t js_transform__write(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Transform"); + ant_value_t transform_fn = 0; + ant_value_t cb_state = 0; + ant_value_t cb = 0; + ant_value_t call_args[3]; + + if (is_err(stream_obj)) return stream_obj; + transform_fn = js_get(js, stream_obj, "_transform"); + + cb_state = js_mkobj(js); + js_set(js, cb_state, "stream", stream_obj); + js_set(js, cb_state, "callback", nargs > 2 ? args[2] : js_mkundef()); + cb = js_heavy_mkfun(js, stream_transform_write_callback, cb_state); + + call_args[0] = nargs > 0 ? args[0] : js_mkundef(); + call_args[1] = nargs > 1 ? args[1] : js_mkstr(js, "utf8", 4); + call_args[2] = cb; + + return stream_call(js, transform_fn, stream_obj, call_args, 3, false); +} + +static ant_value_t stream_transform_final_callback(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t stream_obj = js_get(js, state_obj, "stream"); + ant_value_t callback = js_get(js, state_obj, "callback"); + + if (nargs > 0 && !is_null(args[0]) && !is_undefined(args[0])) { + stream_call_callback(js, callback, &args[0], 1); + return js_mkundef(); + } + + if (nargs > 1 && !is_null(args[1]) && !is_undefined(args[1])) + stream_readable_push_value(js, stream_obj, args[1], js_mkundef()); + stream_readable_push_value(js, stream_obj, js_mknull(), js_mkundef()); + stream_call_callback(js, callback, NULL, 0); + return js_mkundef(); +} + +static ant_value_t js_transform__final(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t stream_obj = stream_require_this(js, js_getthis(js), "Transform"); + ant_value_t flush_fn = 0; + ant_value_t cb_state = 0; + ant_value_t cb = 0; + ant_value_t call_args[1]; + + if (is_err(stream_obj)) return stream_obj; + flush_fn = js_get(js, stream_obj, "_flush"); + if (!is_callable(flush_fn)) { + stream_readable_push_value(js, stream_obj, js_mknull(), js_mkundef()); + stream_call_callback(js, nargs > 0 ? args[0] : js_mkundef(), NULL, 0); + return js_mkundef(); + } + + cb_state = js_mkobj(js); + js_set(js, cb_state, "stream", stream_obj); + js_set(js, cb_state, "callback", nargs > 0 ? args[0] : js_mkundef()); + cb = js_heavy_mkfun(js, stream_transform_final_callback, cb_state); + call_args[0] = cb; + return stream_call(js, flush_fn, stream_obj, call_args, 1, false); +} + +static ant_value_t js_passthrough__transform(ant_t *js, ant_value_t *args, int nargs) { + return js_transform__transform(js, args, nargs); +} + +static ant_value_t stream_finished_cleanup(ant_t *js, ant_value_t state_obj) { + ant_value_t stream_obj = js_get(js, state_obj, "stream"); + if (stream_is_instance(stream_obj) || is_object_type(stream_obj)) { + stream_remove_listener(js, stream_obj, "end", js_get(js, state_obj, "onFinish")); + stream_remove_listener(js, stream_obj, "finish", js_get(js, state_obj, "onFinish")); + stream_remove_listener(js, stream_obj, "close", js_get(js, state_obj, "onFinish")); + stream_remove_listener(js, stream_obj, "error", js_get(js, state_obj, "onError")); + } + return js_mkundef(); +} + +static ant_value_t stream_finished_fire(ant_t *js, ant_value_t state_obj, ant_value_t error) { + ant_value_t called = js_get(js, state_obj, "called"); + ant_value_t callback = js_get(js, state_obj, "callback"); + ant_value_t cb_args[1]; + + if (js_truthy(js, called)) return js_mkundef(); + js_set(js, state_obj, "called", js_true); + stream_finished_cleanup(js, state_obj); + + if (is_undefined(error)) stream_call_callback(js, callback, NULL, 0); + else { + cb_args[0] = error; + stream_call_callback(js, callback, cb_args, 1); + } + return js_mkundef(); +} + +static ant_value_t stream_finished_on_finish(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + return stream_finished_fire(js, state_obj, js_mkundef()); +} + +static ant_value_t stream_finished_on_error(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t error = nargs > 0 ? args[0] : js_mkundef(); + return stream_finished_fire(js, state_obj, error); +} + +static ant_value_t stream_finished_register(ant_t *js, ant_value_t stream_obj, ant_value_t callback) { + ant_value_t state_obj = js_mkobj(js); + ant_value_t on_finish = 0; + ant_value_t on_error = 0; + + if (!is_callable(callback)) callback = js_mkfun(stream_noop); + + js_set(js, state_obj, "stream", stream_obj); + js_set(js, state_obj, "callback", callback); + js_set(js, state_obj, "called", js_false); + on_finish = js_heavy_mkfun(js, stream_finished_on_finish, state_obj); + on_error = js_heavy_mkfun(js, stream_finished_on_error, state_obj); + js_set(js, state_obj, "onFinish", on_finish); + js_set(js, state_obj, "onError", on_error); + + eventemitter_add_listener(js, stream_obj, "end", on_finish, false); + eventemitter_add_listener(js, stream_obj, "finish", on_finish, false); + eventemitter_add_listener(js, stream_obj, "close", on_finish, false); + eventemitter_add_listener(js, stream_obj, "error", on_error, false); + return stream_obj; +} + +static ant_value_t js_stream_finished(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t callback = nargs > 1 ? args[1] : js_mkundef(); + if (nargs < 1 || !is_object_type(args[0])) return js_mkerr(js, "finished requires a stream"); + return stream_finished_register(js, args[0], callback); +} + +static ant_value_t stream_pipeline_done(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t callback = js_get(js, state_obj, "callback"); + ant_value_t called = js_get(js, state_obj, "called"); + ant_value_t cb_args[1]; + + if (js_truthy(js, called)) return js_mkundef(); + js_set(js, state_obj, "called", js_true); + + if (nargs > 0 && !is_undefined(args[0])) { + cb_args[0] = args[0]; + stream_call_callback(js, callback, cb_args, 1); + } else stream_call_callback(js, callback, NULL, 0); + return js_mkundef(); +} + +static ant_value_t stream_pipeline_error(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t done = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (nargs > 0 && !is_undefined(args[0])) stream_call_callback(js, done, &args[0], 1); + return js_mkundef(); +} + +static ant_value_t stream_pipeline_schedule_done(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t done = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + stream_call_callback(js, done, NULL, 0); + return js_mkundef(); +} + +static ant_value_t js_stream_pipeline(ant_t *js, ant_value_t *args, int nargs) { + int stream_count = nargs; + ant_value_t callback = js_mkundef(); + ant_value_t done_state = 0; + ant_value_t done = 0; + + if (nargs > 0 && is_callable(args[nargs - 1])) { + callback = args[nargs - 1]; + stream_count--; + } + + if (!is_callable(callback)) callback = js_mkfun(stream_noop); + if (stream_count <= 0) return js_mkundef(); + + done_state = js_mkobj(js); + js_set(js, done_state, "callback", callback); + js_set(js, done_state, "called", js_false); + done = js_heavy_mkfun(js, stream_pipeline_done, done_state); + + if (stream_count < 2) { + stream_schedule_microtask(js, stream_pipeline_schedule_done, done); + return args[0]; + } + + for (int i = 0; i < stream_count - 1; i++) { + ant_value_t error_cb = js_heavy_mkfun(js, stream_pipeline_error, done); + ant_value_t finished_args[2]; + + finished_args[0] = args[i]; + finished_args[1] = error_cb; + js_stream_finished(js, finished_args, 2); + + ant_value_t pipe_args[2]; + pipe_args[0] = args[i + 1]; + pipe_args[1] = js_mkundef(); + stream_call_prop(js, args[i], "pipe", pipe_args, 2); + } + + { + ant_value_t finished_args[2]; + finished_args[0] = args[stream_count - 1]; + finished_args[1] = done; + js_stream_finished(js, finished_args, 2); + } + + return args[stream_count - 1]; +} + +static ant_value_t stream_promise_callback(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t promise = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (nargs > 0 && !is_undefined(args[0])) js_reject_promise(js, promise, args[0]); + else js_resolve_promise(js, promise, js_mkundef()); + return js_mkundef(); +} + +static ant_value_t js_stream_promises_finished(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t promise = js_mkpromise(js); + ant_value_t finished_args[2]; + if (nargs < 1 || !is_object_type(args[0])) { + js_reject_promise(js, promise, js_mkerr(js, "finished requires a stream")); + return promise; + } + finished_args[0] = args[0]; + finished_args[1] = js_heavy_mkfun(js, stream_promise_callback, promise); + js_stream_finished(js, finished_args, 2); + return promise; +} + +static ant_value_t js_stream_promises_pipeline(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t promise = js_mkpromise(js); + ant_value_t *call_args = NULL; + + if (nargs <= 0) { + js_resolve_promise(js, promise, js_mkundef()); + return promise; + } + + call_args = malloc((size_t)(nargs + 1) * sizeof(*call_args)); + if (!call_args) { + js_reject_promise(js, promise, js_mkerr(js, "out of memory")); + return promise; + } + + for (int i = 0; i < nargs; i++) call_args[i] = args[i]; + call_args[nargs] = js_heavy_mkfun(js, stream_promise_callback, promise); + js_stream_pipeline(js, call_args, nargs + 1); + free(call_args); + return promise; +} + +static void stream_release_reader(ant_t *js, ant_value_t state_obj) { + ant_value_t reader = js_get(js, state_obj, "reader"); + if (!is_object_type(reader)) return; + stream_call_prop(js, reader, "releaseLock", NULL, 0); +} + +static ant_value_t stream_readable_from_step(ant_t *js, ant_value_t *args, int nargs); + +static void stream_readable_from_schedule(ant_t *js, ant_value_t state_obj) { + stream_schedule_microtask(js, stream_readable_from_step, state_obj); +} + +static ant_value_t stream_readable_from_fail(ant_t *js, ant_value_t state_obj, ant_value_t error) { + ant_value_t readable = js_get(js, state_obj, "readable"); + stream_release_reader(js, state_obj); + if (stream_is_instance(readable)) { + ant_value_t destroy_args[1] = { error }; + ant_value_t saved_this = js->this_val; + js->this_val = readable; + js_stream_destroy(js, destroy_args, 1); + js->this_val = saved_this; + } + return js_mkundef(); +} + +static ant_value_t stream_readable_from_handle_result(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t readable = js_get(js, state_obj, "readable"); + ant_value_t result = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t done = 0; + ant_value_t value = 0; + + if (js_truthy(js, js_get(js, readable, "destroyed"))) { + stream_release_reader(js, state_obj); + return js_mkundef(); + } + + if (!is_object_type(result)) return stream_readable_from_fail(js, state_obj, js_mkerr(js, "iterator step must be an object")); + done = js_get(js, result, "done"); + value = js_get(js, result, "value"); + if (js_truthy(js, done)) { + stream_release_reader(js, state_obj); + stream_readable_push_value(js, readable, js_mknull(), js_mkundef()); + return js_mkundef(); + } + + stream_readable_push_value(js, readable, value, js_mkundef()); + if (!js_truthy(js, js_get(js, readable, "destroyed"))) stream_readable_from_schedule(js, state_obj); + return js_mkundef(); +} + +static ant_value_t stream_readable_from_reject(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t error = nargs > 0 ? args[0] : js_mkerr(js, "stream iteration failed"); + return stream_readable_from_fail(js, state_obj, error); +} + +static ant_value_t stream_readable_from_step(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t readable = js_get(js, state_obj, "readable"); + ant_value_t mode = js_get(js, state_obj, "mode"); + ant_value_t iterator = js_get(js, state_obj, "iterator"); + + ant_value_t next_result = js_mkundef(); + ant_value_t on_resolve = js_heavy_mkfun(js, stream_readable_from_handle_result, state_obj); + ant_value_t on_reject = js_heavy_mkfun(js, stream_readable_from_reject, state_obj); + ant_value_t then_result = 0; + + if (js_truthy(js, js_get(js, readable, "destroyed"))) { + stream_release_reader(js, state_obj); + return js_mkundef(); + } + + if (stream_key_is_cstr(js, mode, "reader")) next_result = stream_call_prop(js, iterator, "read", NULL, 0); + else next_result = stream_call_prop(js, iterator, "next", NULL, 0); + + if (is_err(next_result)) return stream_readable_from_fail(js, state_obj, next_result); + if (vtype(next_result) == T_PROMISE) { + then_result = js_promise_then(js, next_result, on_resolve, on_reject); + promise_mark_handled(then_result); + return js_mkundef(); + } + + ant_value_t one_arg[1] = { next_result }; + return stream_readable_from_handle_result(js, one_arg, 1); +} + +static ant_value_t stream_readable_from_start(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t state_obj = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + ant_value_t readable = js_get(js, state_obj, "readable"); + ant_value_t source = js_get(js, state_obj, "source"); + + ant_value_t async_iter_fn = 0; + ant_value_t reader_fn = 0; + js_iter_t it; + + if (js_truthy(js, js_get(js, readable, "destroyed"))) return js_mkundef(); + + async_iter_fn = is_object_type(source) ? js_get_sym(js, source, get_asyncIterator_sym()) : js_mkundef(); + if (is_callable(async_iter_fn)) { + ant_value_t iterator = stream_call(js, async_iter_fn, source, NULL, 0, false); + if (is_err(iterator)) return stream_readable_from_fail(js, state_obj, iterator); + js_set(js, state_obj, "iterator", iterator); + js_set(js, state_obj, "mode", js_mkstr(js, "async", 5)); + stream_readable_from_schedule(js, state_obj); + return js_mkundef(); + } + + if (js_iter_open(js, source, &it)) { + ant_value_t value = 0; + while (js_iter_next(js, &it, &value)) { + if (js_truthy(js, js_get(js, readable, "destroyed"))) break; + stream_readable_push_value(js, readable, value, js_mkundef()); + } + js_iter_close(js, &it); + if (!js_truthy(js, js_get(js, readable, "destroyed"))) + stream_readable_push_value(js, readable, js_mknull(), js_mkundef()); + return js_mkundef(); + } + + reader_fn = is_object_type(source) ? js_get(js, source, "getReader") : js_mkundef(); + if (is_callable(reader_fn)) { + ant_value_t reader = stream_call(js, reader_fn, source, NULL, 0, false); + if (is_err(reader)) return stream_readable_from_fail(js, state_obj, reader); + js_set(js, state_obj, "reader", reader); + js_set(js, state_obj, "iterator", reader); + js_set(js, state_obj, "mode", js_mkstr(js, "reader", 6)); + stream_readable_from_schedule(js, state_obj); + return js_mkundef(); + } + + if (!is_undefined(source)) stream_readable_push_value(js, readable, source, js_mkundef()); + stream_readable_push_value(js, readable, js_mknull(), js_mkundef()); + + return js_mkundef(); +} + +static ant_value_t js_readable_from(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t ctor_args[1]; + ant_value_t readable = 0; + ant_value_t state_obj = 0; + + ctor_args[0] = nargs > 1 ? args[1] : js_mkundef(); + readable = stream_construct(js, g_readable_proto, ctor_args[0], stream_init_readable); + if (is_err(readable)) return readable; + + state_obj = js_mkobj(js); + js_set(js, state_obj, "readable", readable); + js_set(js, state_obj, "source", nargs > 0 ? args[0] : js_mkundef()); + js_set(js, state_obj, "iterator", js_mkundef()); + js_set(js, state_obj, "reader", js_mkundef()); + js_set(js, state_obj, "mode", js_mkundef()); + stream_schedule_microtask(js, stream_readable_from_start, state_obj); + + return readable; +} + +static ant_value_t js_readable_from_web(ant_t *js, ant_value_t *args, int nargs) { + return js_readable_from(js, args, nargs); +} + +static ant_value_t js_stream_ctor(ant_t *js, ant_value_t *args, int nargs) { + return stream_construct(js, g_stream_proto, nargs > 0 ? args[0] : js_mkundef(), stream_init_base); +} + +static ant_value_t js_readable_ctor(ant_t *js, ant_value_t *args, int nargs) { + return stream_construct(js, g_readable_proto, nargs > 0 ? args[0] : js_mkundef(), stream_init_readable); +} + +static ant_value_t js_writable_ctor(ant_t *js, ant_value_t *args, int nargs) { + return stream_construct(js, g_writable_proto, nargs > 0 ? args[0] : js_mkundef(), stream_init_writable); +} + +static ant_value_t js_duplex_ctor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t proto = js_instance_proto_from_new_target(js, g_duplex_proto); + ant_value_t obj = stream_make_base_object(js, is_object_type(proto) ? proto : g_duplex_proto); + ant_value_t options = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t options_obj = is_object_type(options) ? options : js_mkobj(js); + + stream_init_readable(js, obj, options); + stream_init_writable(js, obj, options); + + js_set(js, obj, "readable", js_true); + js_set(js, obj, "writable", js_true); + js_set(js, obj, "allowHalfOpen", js_bool(stream_get_option(js, options_obj, "allowHalfOpen") != js_false)); + + return obj; +} + +static ant_value_t js_transform_ctor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t proto = js_instance_proto_from_new_target(js, g_transform_proto); + ant_value_t obj = stream_make_base_object(js, is_object_type(proto) ? proto : g_transform_proto); + ant_value_t options = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t options_obj = is_object_type(options) ? options : js_mkobj(js); + + ant_value_t transform_fn = 0; + ant_value_t flush_fn = 0; + + stream_init_readable(js, obj, options); + stream_init_writable(js, obj, options); + + js_set(js, obj, "readable", js_true); + js_set(js, obj, "writable", js_true); + js_set(js, obj, "allowHalfOpen", js_bool(stream_get_option(js, options_obj, "allowHalfOpen") != js_false)); + + transform_fn = stream_get_option(js, options_obj, "transform"); + flush_fn = stream_get_option(js, options_obj, "flush"); + + if (is_callable(transform_fn)) js_set(js, obj, "_transform", transform_fn); + if (is_callable(flush_fn)) js_set(js, obj, "_flush", flush_fn); + + return obj; +} + +static ant_value_t js_passthrough_ctor(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t proto = js_instance_proto_from_new_target(js, g_passthrough_proto); + ant_value_t obj = stream_make_base_object(js, is_object_type(proto) ? proto : g_passthrough_proto); + ant_value_t options = nargs > 0 ? args[0] : js_mkundef(); + ant_value_t options_obj = is_object_type(options) ? options : js_mkobj(js); + + stream_init_readable(js, obj, options); + stream_init_writable(js, obj, options); + + js_set(js, obj, "readable", js_true); + js_set(js, obj, "writable", js_true); + js_set(js, obj, "allowHalfOpen", js_bool(stream_get_option(js, options_obj, "allowHalfOpen") != js_false)); + + return obj; +} + +void stream_init_constructors(ant_t *js) { + ant_value_t events = 0; + ant_value_t ee_ctor = 0; + ant_value_t ee_proto = 0; + + if (g_stream_ctor) return; + + events = events_library(js); + ee_ctor = js_get(js, events, "EventEmitter"); + ee_proto = js_get(js, ee_ctor, "prototype"); + + g_stream_proto = js_mkobj(js); + js_set_proto_init(g_stream_proto, ee_proto); + js_set(js, g_stream_proto, "pipe", js_mkfun(js_stream_pipe)); + js_set(js, g_stream_proto, "unpipe", js_mkfun(js_stream_unpipe)); + js_set(js, g_stream_proto, "pause", js_mkfun(js_stream_pause)); + js_set(js, g_stream_proto, "resume", js_mkfun(js_stream_resume)); + js_set(js, g_stream_proto, "isPaused", js_mkfun(js_stream_is_paused)); + js_set(js, g_stream_proto, "destroy", js_mkfun(js_stream_destroy)); + js_set_sym(js, g_stream_proto, get_toStringTag_sym(), js_mkstr(js, "Stream", 6)); + g_stream_ctor = js_make_ctor(js, js_stream_ctor, g_stream_proto, "Stream", 6); + + g_readable_proto = js_mkobj(js); + js_set_proto_init(g_readable_proto, g_stream_proto); + js_set(js, g_readable_proto, "_read", js_mkfun(js_readable__read)); + js_set(js, g_readable_proto, "push", js_mkfun(js_readable_push)); + js_set(js, g_readable_proto, "read", js_mkfun(js_readable_read)); + js_set(js, g_readable_proto, "on", js_mkfun(js_readable_on)); + js_set(js, g_readable_proto, "resume", js_mkfun(js_readable_resume)); + js_set(js, g_readable_proto, "pause", js_mkfun(js_readable_pause)); + js_set_sym(js, g_readable_proto, get_toStringTag_sym(), js_mkstr(js, "Readable", 8)); + g_readable_ctor = js_make_ctor(js, js_readable_ctor, g_readable_proto, "Readable", 8); + js_set(js, g_readable_ctor, "from", js_mkfun(js_readable_from)); + js_set(js, g_readable_ctor, "fromWeb", js_mkfun(js_readable_from_web)); + + g_writable_proto = js_mkobj(js); + js_set_proto_init(g_writable_proto, g_stream_proto); + js_set(js, g_writable_proto, "_write", js_mkfun(js_writable__write)); + js_set(js, g_writable_proto, "_final", js_mkfun(js_writable__final)); + js_set(js, g_writable_proto, "write", js_mkfun(js_writable_write)); + js_set(js, g_writable_proto, "end", js_mkfun(js_writable_end)); + js_set(js, g_writable_proto, "cork", js_mkfun(stream_noop)); + js_set(js, g_writable_proto, "uncork", js_mkfun(stream_noop)); + js_set_sym(js, g_writable_proto, get_toStringTag_sym(), js_mkstr(js, "Writable", 8)); + g_writable_ctor = js_make_ctor(js, js_writable_ctor, g_writable_proto, "Writable", 8); + + g_duplex_proto = js_mkobj(js); + js_set_proto_init(g_duplex_proto, g_readable_proto); + js_set(js, g_duplex_proto, "_write", js_mkfun(js_writable__write)); + js_set(js, g_duplex_proto, "_final", js_mkfun(js_writable__final)); + js_set(js, g_duplex_proto, "write", js_mkfun(js_writable_write)); + js_set(js, g_duplex_proto, "end", js_mkfun(js_writable_end)); + js_set(js, g_duplex_proto, "cork", js_mkfun(stream_noop)); + js_set(js, g_duplex_proto, "uncork", js_mkfun(stream_noop)); + js_set_sym(js, g_duplex_proto, get_toStringTag_sym(), js_mkstr(js, "Duplex", 6)); + g_duplex_ctor = js_make_ctor(js, js_duplex_ctor, g_duplex_proto, "Duplex", 6); + + g_transform_proto = js_mkobj(js); + js_set_proto_init(g_transform_proto, g_duplex_proto); + js_set(js, g_transform_proto, "_transform", js_mkfun(js_transform__transform)); + js_set(js, g_transform_proto, "_write", js_mkfun(js_transform__write)); + js_set(js, g_transform_proto, "_final", js_mkfun(js_transform__final)); + js_set_sym(js, g_transform_proto, get_toStringTag_sym(), js_mkstr(js, "Transform", 9)); + g_transform_ctor = js_make_ctor(js, js_transform_ctor, g_transform_proto, "Transform", 9); + + g_passthrough_proto = js_mkobj(js); + js_set_proto_init(g_passthrough_proto, g_transform_proto); + js_set(js, g_passthrough_proto, "_transform", js_mkfun(js_passthrough__transform)); + js_set_sym(js, g_passthrough_proto, get_toStringTag_sym(), js_mkstr(js, "PassThrough", 11)); + g_passthrough_ctor = js_make_ctor(js, js_passthrough_ctor, g_passthrough_proto, "PassThrough", 11); + + gc_register_root(&g_stream_proto); + gc_register_root(&g_stream_ctor); + gc_register_root(&g_readable_proto); + gc_register_root(&g_readable_ctor); + gc_register_root(&g_writable_proto); + gc_register_root(&g_writable_ctor); + gc_register_root(&g_duplex_proto); + gc_register_root(&g_duplex_ctor); + gc_register_root(&g_transform_proto); + gc_register_root(&g_transform_ctor); + gc_register_root(&g_passthrough_proto); + gc_register_root(&g_passthrough_ctor); +} + +ant_value_t stream_readable_constructor(ant_t *js) { + stream_init_constructors(js); + return g_readable_ctor; +} + +ant_value_t stream_writable_constructor(ant_t *js) { + stream_init_constructors(js); + return g_writable_ctor; +} + +ant_value_t stream_readable_prototype(ant_t *js) { + stream_init_constructors(js); + return g_readable_proto; +} + +ant_value_t stream_writable_prototype(ant_t *js) { + stream_init_constructors(js); + return g_writable_proto; +} + +ant_value_t stream_construct_readable(ant_t *js, ant_value_t base_proto, ant_value_t options) { + stream_init_constructors(js); + return stream_construct(js, base_proto, options, stream_init_readable); +} + +ant_value_t stream_construct_writable(ant_t *js, ant_value_t base_proto, ant_value_t options) { + stream_init_constructors(js); + return stream_construct(js, base_proto, options, stream_init_writable); +} + +void stream_init_readable_object(ant_t *js, ant_value_t obj, ant_value_t options) { + stream_init_constructors(js); + if (!is_object_type(obj)) return; + js_set_native_tag(obj, STREAM_NATIVE_TAG); + stream_init_readable(js, obj, options); +} + +void stream_init_writable_object(ant_t *js, ant_value_t obj, ant_value_t options) { + stream_init_constructors(js); + if (!is_object_type(obj)) return; + js_set_native_tag(obj, STREAM_NATIVE_TAG); + stream_init_writable(js, obj, options); +} + +ant_value_t stream_readable_push(ant_t *js, ant_value_t stream_obj, ant_value_t chunk, ant_value_t encoding) { + stream_init_constructors(js); + return stream_readable_push_value(js, stream_obj, chunk, encoding); +} + +ant_value_t stream_library(ant_t *js) { + ant_value_t lib = js_mkobj(js); + ant_value_t promises = js_mkobj(js); + stream_init_constructors(js); + + js_set(js, promises, "pipeline", js_mkfun(js_stream_promises_pipeline)); + js_set(js, promises, "finished", js_mkfun(js_stream_promises_finished)); + + js_set_module_default(js, lib, g_stream_ctor, "Stream"); + js_set(js, lib, "Readable", g_readable_ctor); + js_set(js, lib, "Writable", g_writable_ctor); + js_set(js, lib, "Duplex", g_duplex_ctor); + js_set(js, lib, "Transform", g_transform_ctor); + js_set(js, lib, "PassThrough", g_passthrough_ctor); + js_set(js, lib, "pipeline", js_mkfun(js_stream_pipeline)); + js_set(js, lib, "finished", js_mkfun(js_stream_finished)); + js_set(js, lib, "promises", promises); + + js_set(js, g_stream_ctor, "Readable", g_readable_ctor); + js_set(js, g_stream_ctor, "Writable", g_writable_ctor); + js_set(js, g_stream_ctor, "Duplex", g_duplex_ctor); + js_set(js, g_stream_ctor, "Transform", g_transform_ctor); + js_set(js, g_stream_ctor, "PassThrough", g_passthrough_ctor); + js_set(js, g_stream_ctor, "pipeline", js_get(js, lib, "pipeline")); + js_set(js, g_stream_ctor, "finished", js_get(js, lib, "finished")); + js_set(js, g_stream_ctor, "promises", promises); + + js_set(js, promises, "default", promises); + js_set_slot_wb(js, promises, SLOT_DEFAULT, promises); + js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "stream", 6)); + + return lib; +} + +ant_value_t stream_promises_library(ant_t *js) { + ant_value_t stream_ns = js_esm_import_sync_cstr(js, "stream", 6); + ant_value_t promises = 0; + + if (is_err(stream_ns)) return stream_ns; + promises = js_get(js, stream_ns, "promises"); + + if (!is_object_type(promises)) return js_mkerr(js, "stream.promises is not available"); + js_set(js, promises, "default", promises); + js_set_slot_wb(js, promises, SLOT_DEFAULT, promises); + + return promises; +} diff --git a/src/modules/timer.c b/src/modules/timer.c index 60a6850..615f879 100644 --- a/src/modules/timer.c +++ b/src/modules/timer.c @@ -687,6 +687,31 @@ int has_pending_microtasks(void) { return timer_state.microtasks != NULL ? 1 : 0; } +static void timers_define_common(ant_t *js, ant_value_t obj) { + js_set(js, obj, "setTimeout", js_mkfun(js_set_timeout)); + js_set(js, obj, "clearTimeout", js_mkfun(js_clear_timeout)); + js_set(js, obj, "setInterval", js_mkfun(js_set_interval)); + js_set(js, obj, "clearInterval", js_mkfun(js_clear_timeout)); + js_set(js, obj, "setImmediate", js_mkfun(js_set_immediate)); + js_set(js, obj, "clearImmediate", js_mkfun(js_clear_immediate)); + js_set(js, obj, "queueMicrotask", js_mkfun(js_queue_microtask)); +} + +void init_timer_module() { + ant_t *js = rt->js; + timer_state.js = js; + timers_define_common(js, js_glob(js)); +} + +ant_value_t timers_library(ant_t *js) { + ant_value_t lib = js_mkobj(js); + + timers_define_common(js, lib); + js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "timers", 6)); + + return lib; +} + // TODO: mostly stubbed ant_value_t timers_promises_library(ant_t *js) { ant_value_t lib = js_mkobj(js); @@ -703,21 +728,6 @@ ant_value_t timers_promises_library(ant_t *js) { return lib; } -void init_timer_module() { - ant_t *js = rt->js; - - timer_state.js = js; - ant_value_t global = js_glob(js); - - js_set(js, global, "setTimeout", js_mkfun(js_set_timeout)); - js_set(js, global, "clearTimeout", js_mkfun(js_clear_timeout)); - js_set(js, global, "setInterval", js_mkfun(js_set_interval)); - js_set(js, global, "clearInterval", js_mkfun(js_clear_timeout)); - js_set(js, global, "setImmediate", js_mkfun(js_set_immediate)); - js_set(js, global, "clearImmediate", js_mkfun(js_clear_immediate)); - js_set(js, global, "queueMicrotask", js_mkfun(js_queue_microtask)); -} - void gc_mark_timers(ant_t *js, gc_mark_fn mark) { for (timer_entry_t *t = timer_state.timers; t; t = t->next) { mark(js, t->callback); diff --git a/src/modules/tty.c b/src/modules/tty.c index ba3d539..33d8f0e 100644 --- a/src/modules/tty.c +++ b/src/modules/tty.c @@ -27,6 +27,7 @@ #endif #include "ant.h" +#include "gc/roots.h" #include "descriptors.h" #include "errors.h" #include "internal.h" @@ -34,9 +35,15 @@ #include "tty_ctrl.h" #include "silver/engine.h" +#include "modules/stream.h" #include "modules/symbol.h" #include "modules/tty.h" +static ant_value_t g_tty_readstream_proto = 0; +static ant_value_t g_tty_readstream_ctor = 0; +static ant_value_t g_tty_writestream_proto = 0; +static ant_value_t g_tty_writestream_ctor = 0; + static bool parse_fd(ant_value_t value, int *fd_out) { int fd = 0; if (!tty_ctrl_parse_int_value(value, &fd)) return false; @@ -588,10 +595,8 @@ static ant_value_t tty_read_stream_constructor(ant_t *js, ant_value_t *args, int } } - ant_value_t ctor = js_getcurrentfunc(js); - ant_value_t proto = js_get(js, ctor, "prototype"); - ant_value_t obj = js_mkobj(js); - if (is_special_object(proto)) js_set_proto_init(obj, proto); + ant_value_t obj = stream_construct_readable(js, g_tty_readstream_proto, js_mkundef()); + if (is_err(obj)) return obj; ensure_stream_common_props(js, obj, fd); js_set(js, obj, "isRaw", js_false); @@ -617,10 +622,8 @@ static ant_value_t tty_write_stream_constructor(ant_t *js, ant_value_t *args, in } } - ant_value_t ctor = js_getcurrentfunc(js); - ant_value_t proto = js_get(js, ctor, "prototype"); - ant_value_t obj = js_mkobj(js); - if (is_special_object(proto)) js_set_proto_init(obj, proto); + ant_value_t obj = stream_construct_writable(js, g_tty_writestream_proto, js_mkundef()); + if (is_err(obj)) return obj; ensure_stream_common_props(js, obj, fd); return obj; @@ -648,9 +651,35 @@ static void setup_writestream_proto(ant_t *js, ant_value_t proto) { js_set_sym(js, proto, get_toStringTag_sym(), js_mkstr(js, "WriteStream", 11)); } +static void tty_init_stream_constructors(ant_t *js) { + if (g_tty_readstream_ctor && g_tty_writestream_ctor) return; + stream_init_constructors(js); + + g_tty_readstream_proto = js_mkobj(js); + js_set_proto_init(g_tty_readstream_proto, stream_readable_prototype(js)); + setup_readstream_proto(js, g_tty_readstream_proto); + + g_tty_readstream_ctor = js_make_ctor(js, tty_read_stream_constructor, g_tty_readstream_proto, "ReadStream", 10); + js_set_proto_init(g_tty_readstream_ctor, stream_readable_constructor(js)); + + gc_register_root(&g_tty_readstream_proto); + gc_register_root(&g_tty_readstream_ctor); + + g_tty_writestream_proto = js_mkobj(js); + js_set_proto_init(g_tty_writestream_proto, stream_writable_prototype(js)); + setup_writestream_proto(js, g_tty_writestream_proto); + + g_tty_writestream_ctor = js_make_ctor(js, tty_write_stream_constructor, g_tty_writestream_proto, "WriteStream", 11); + js_set_proto_init(g_tty_writestream_ctor, stream_writable_constructor(js)); + + gc_register_root(&g_tty_writestream_proto); + gc_register_root(&g_tty_writestream_ctor); +} + void init_tty_module(void) { ant_t *js = rt->js; if (!js) return; + tty_init_stream_constructors(js); ant_value_t process_obj = js_get(js, js_glob(js), "process"); if (!is_special_object(process_obj)) return; @@ -666,8 +695,10 @@ void init_tty_module(void) { if (is_callable(native_set_raw)) { js_set(js, stdin_proto, "__antNativeSetRawMode", native_set_raw); } + js_set_proto_init(stdin_proto, stream_readable_prototype(js)); setup_readstream_proto(js, stdin_proto); } + stream_init_readable_object(js, stdin_obj, js_mkundef()); } ant_value_t stdout_obj = js_get(js, process_obj, "stdout"); @@ -677,7 +708,9 @@ void init_tty_module(void) { js_set_getter_desc(js, stdout_obj, "columns", 7, js_mkfun(tty_write_stream_columns_getter), JS_DESC_E | JS_DESC_C); ant_value_t stdout_proto = js_get_proto(js, stdout_obj); + if (is_special_object(stdout_proto)) js_set_proto_init(stdout_proto, stream_writable_prototype(js)); setup_writestream_proto(js, stdout_proto); + stream_init_writable_object(js, stdout_obj, js_mkundef()); } ant_value_t stderr_obj = js_get(js, process_obj, "stderr"); @@ -685,34 +718,21 @@ void init_tty_module(void) { ensure_stream_common_props(js, stderr_obj, ANT_STDERR_FD); js_set_getter_desc(js, stderr_obj, "rows", 4, js_mkfun(tty_write_stream_rows_getter), JS_DESC_E | JS_DESC_C); js_set_getter_desc(js, stderr_obj, "columns", 7, js_mkfun(tty_write_stream_columns_getter), JS_DESC_E | JS_DESC_C); - + ant_value_t stderr_proto = js_get_proto(js, stderr_obj); + if (is_special_object(stderr_proto)) js_set_proto_init(stderr_proto, stream_writable_prototype(js)); setup_writestream_proto(js, stderr_proto); + stream_init_writable_object(js, stderr_obj, js_mkundef()); } } ant_value_t tty_library(ant_t *js) { ant_value_t lib = js_mkobj(js); - - ant_value_t read_ctor_obj = js_mkobj(js); - js_set_slot(read_ctor_obj, SLOT_CFUNC, js_mkfun(tty_read_stream_constructor)); - ant_value_t read_proto = js_mkobj(js); - setup_readstream_proto(js, read_proto); - js_set(js, read_ctor_obj, "prototype", read_proto); - ant_value_t read_ctor = js_obj_to_func(read_ctor_obj); - js_set(js, read_proto, "constructor", read_ctor); - - ant_value_t write_ctor_obj = js_mkobj(js); - js_set_slot(write_ctor_obj, SLOT_CFUNC, js_mkfun(tty_write_stream_constructor)); - ant_value_t write_proto = js_mkobj(js); - setup_writestream_proto(js, write_proto); - js_set(js, write_ctor_obj, "prototype", write_proto); - ant_value_t write_ctor = js_obj_to_func(write_ctor_obj); - js_set(js, write_proto, "constructor", write_ctor); + tty_init_stream_constructors(js); js_set(js, lib, "isatty", js_mkfun(tty_isatty)); - js_set(js, lib, "ReadStream", read_ctor); - js_set(js, lib, "WriteStream", write_ctor); + js_set(js, lib, "ReadStream", g_tty_readstream_ctor); + js_set(js, lib, "WriteStream", g_tty_writestream_ctor); js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "tty", 3)); return lib; diff --git a/src/modules/util.c b/src/modules/util.c index ac1741a..507ba69 100644 --- a/src/modules/util.c +++ b/src/modules/util.c @@ -11,11 +11,15 @@ #include "ant.h" #include "internal.h" +#include "esm/library.h" #include "silver/engine.h" +#include "modules/buffer.h" +#include "modules/date.h" #include "modules/json.h" #include "modules/symbol.h" #include "modules/util.h" +#include "modules/collections.h" typedef struct { char *buf; @@ -210,6 +214,304 @@ static ant_value_t util_inspect(ant_t *js, ant_value_t *args, int nargs) { return out; } +static bool util_has_proto_in_chain(ant_t *js, ant_value_t value, ant_value_t proto) { + if (!is_special_object(proto)) return false; + + ant_value_t current = value; + while (is_special_object(current)) { + current = js_get_proto(js, current); + if (current == proto) return true; + } + + return false; +} + +static bool util_is_typed_array_value(ant_value_t value, TypedArrayData **out) { + ant_value_t slot; + + if (vtype(value) == T_TYPEDARRAY) { + if (out) *out = (TypedArrayData *)js_gettypedarray(value); + return true; + } + + if (!is_object_type(value)) return false; + slot = js_get_slot(value, SLOT_BUFFER); + if (vtype(slot) != T_TYPEDARRAY) return false; + + if (out) *out = (TypedArrayData *)js_gettypedarray(slot); + return true; +} + +static ArrayBufferData *util_get_arraybuffer_data(ant_value_t value) { + ant_value_t slot; + + if (!is_object_type(value) || buffer_is_dataview(value)) return NULL; + slot = js_get_slot(value, SLOT_BUFFER); + if (vtype(slot) != T_NUM) return NULL; + + return (ArrayBufferData *)(uintptr_t)(size_t)js_getnum(slot); +} + +static bool util_is_boxed_primitive(ant_value_t value, uint8_t *type_out) { + ant_value_t primitive; + + if (!is_object_type(value)) return false; + primitive = js_get_slot(value, SLOT_PRIMITIVE); + if (vtype(primitive) == T_UNDEF) return false; + + if (type_out) *type_out = vtype(primitive); + return true; +} + +static bool util_has_to_string_tag(ant_t *js, ant_value_t value, const char *tag, size_t tag_len) { + ant_value_t to_string_tag; + size_t actual_len = 0; + const char *actual; + + if (!is_object_type(value)) return false; + + to_string_tag = js_get_sym(js, value, get_toStringTag_sym()); + if (vtype(to_string_tag) != T_STR) return false; + + actual = js_getstr(js, to_string_tag, &actual_len); + return actual != NULL && actual_len == tag_len && memcmp(actual, tag, tag_len) == 0; +} + +static bool util_is_arguments_object_value(ant_t *js, ant_value_t value) { + ant_value_t callee = js_mkundef(); + + if (vtype(value) != T_ARR) return false; + if (vtype(js_get_slot(value, SLOT_STRICT_ARGS)) != T_UNDEF) return true; + if (!util_has_to_string_tag(js, value, "Arguments", 9)) return false; + + return js_try_get_own_data_prop(js, value, "callee", 6, &callee); +} + +static ant_value_t util_types_is_any_array_buffer(ant_t *js, ant_value_t *args, int nargs) { + ArrayBufferData *buffer = (nargs > 0) ? util_get_arraybuffer_data(args[0]) : NULL; + return js_bool(buffer != NULL); +} + +static ant_value_t util_types_is_array_buffer(ant_t *js, ant_value_t *args, int nargs) { + ArrayBufferData *buffer = (nargs > 0) ? util_get_arraybuffer_data(args[0]) : NULL; + return js_bool(buffer != NULL && !buffer->is_shared); +} + +static ant_value_t util_types_is_shared_array_buffer(ant_t *js, ant_value_t *args, int nargs) { + ArrayBufferData *buffer = (nargs > 0) ? util_get_arraybuffer_data(args[0]) : NULL; + return js_bool(buffer != NULL && buffer->is_shared); +} + +static ant_value_t util_types_is_array_buffer_view(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_false; + return js_bool(buffer_is_dataview(args[0]) || util_is_typed_array_value(args[0], NULL)); +} + +static ant_value_t util_types_is_data_view(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_false; + return js_bool(buffer_is_dataview(args[0])); +} + +static ant_value_t util_types_is_typed_array(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_false; + return js_bool(util_is_typed_array_value(args[0], NULL)); +} + +static ant_value_t util_types_is_float16_array(ant_t *js, ant_value_t *args, int nargs) { + TypedArrayData *typed_array = NULL; + if (nargs < 1 || !util_is_typed_array_value(args[0], &typed_array)) return js_false; + return js_bool(typed_array != NULL && typed_array->type == TYPED_ARRAY_FLOAT16); +} + +#define DEFINE_TYPED_ARRAY_CHECK(fn_name, typed_array_kind) \ + static ant_value_t fn_name(ant_t *js, ant_value_t *args, int nargs) { \ + TypedArrayData *typed_array = NULL; \ + if (nargs < 1 || !util_is_typed_array_value(args[0], &typed_array)) return js_false; \ + return js_bool(typed_array != NULL && typed_array->type == typed_array_kind); \ + } + +DEFINE_TYPED_ARRAY_CHECK(util_types_is_int8_array, TYPED_ARRAY_INT8) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_uint8_array, TYPED_ARRAY_UINT8) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_uint8_clamped_array, TYPED_ARRAY_UINT8_CLAMPED) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_int16_array, TYPED_ARRAY_INT16) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_uint16_array, TYPED_ARRAY_UINT16) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_int32_array, TYPED_ARRAY_INT32) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_uint32_array, TYPED_ARRAY_UINT32) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_float32_array, TYPED_ARRAY_FLOAT32) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_float64_array, TYPED_ARRAY_FLOAT64) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_bigint64_array, TYPED_ARRAY_BIGINT64) +DEFINE_TYPED_ARRAY_CHECK(util_types_is_biguint64_array, TYPED_ARRAY_BIGUINT64) + +static ant_value_t util_types_is_promise(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_false; + return js_bool(vtype(args[0]) == T_PROMISE); +} + +static ant_value_t util_types_is_proxy(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || !is_object_type(args[0])) return js_false; + return js_bool(is_proxy(args[0])); +} + +static ant_value_t util_types_is_regexp(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t regexp_proto; + if (nargs < 1 || !is_object_type(args[0])) return js_false; + regexp_proto = js_get_ctor_proto(js, "RegExp", 6); + return js_bool(util_has_proto_in_chain(js, args[0], regexp_proto)); +} + +static ant_value_t util_types_is_date(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_false; + return js_bool(is_date_instance(args[0])); +} + +static ant_value_t util_types_is_map(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || vtype(args[0]) != T_OBJ) return js_false; + return js_bool(js_obj_ptr(args[0])->type_tag == T_MAP); +} + +static ant_value_t util_types_is_set(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || vtype(args[0]) != T_OBJ) return js_false; + return js_bool(js_obj_ptr(args[0])->type_tag == T_SET); +} + +static ant_value_t util_types_is_weak_map(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || vtype(args[0]) != T_OBJ) return js_false; + return js_bool(js_obj_ptr(args[0])->type_tag == T_WEAKMAP); +} + +static ant_value_t util_types_is_weak_set(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || vtype(args[0]) != T_OBJ) return js_false; + return js_bool(js_obj_ptr(args[0])->type_tag == T_WEAKSET); +} + +static ant_value_t util_types_is_async_function(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t func_obj; + if (nargs < 1 || vtype(args[0]) != T_FUNC) return js_false; + func_obj = js_func_obj(args[0]); + return js_bool(js_get_slot(func_obj, SLOT_ASYNC) == js_true); +} + +static ant_value_t util_types_is_generator_function(ant_t *js, ant_value_t *args, int nargs) { + sv_closure_t *closure; + + if (nargs < 1 || vtype(args[0]) != T_FUNC) return js_false; + + closure = js_func_closure(args[0]); + return js_bool(closure != NULL && closure->func != NULL && closure->func->is_generator); +} + +static ant_value_t util_types_is_generator_object(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_false; + return js_bool(vtype(args[0]) == T_GENERATOR); +} + +static ant_value_t util_types_is_arguments_object(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_false; + return js_bool(util_is_arguments_object_value(js, args[0])); +} + +static ant_value_t util_types_is_native_error(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || !is_object_type(args[0])) return js_false; + return js_bool(js_get_slot(args[0], SLOT_ERROR_BRAND) == js_true); +} + +static ant_value_t util_types_is_boxed_primitive(ant_t *js, ant_value_t *args, int nargs) { + return js_bool(nargs > 0 && util_is_boxed_primitive(args[0], NULL)); +} + +static ant_value_t util_types_is_boolean_object(ant_t *js, ant_value_t *args, int nargs) { + uint8_t type = T_UNDEF; + return js_bool(nargs > 0 && util_is_boxed_primitive(args[0], &type) && type == T_BOOL); +} + +static ant_value_t util_types_is_number_object(ant_t *js, ant_value_t *args, int nargs) { + uint8_t type = T_UNDEF; + return js_bool(nargs > 0 && util_is_boxed_primitive(args[0], &type) && type == T_NUM); +} + +static ant_value_t util_types_is_string_object(ant_t *js, ant_value_t *args, int nargs) { + uint8_t type = T_UNDEF; + return js_bool(nargs > 0 && util_is_boxed_primitive(args[0], &type) && type == T_STR); +} + +static ant_value_t util_types_is_symbol_object(ant_t *js, ant_value_t *args, int nargs) { + uint8_t type = T_UNDEF; + return js_bool(nargs > 0 && util_is_boxed_primitive(args[0], &type) && type == T_SYMBOL); +} + +static ant_value_t util_types_is_bigint_object(ant_t *js, ant_value_t *args, int nargs) { + uint8_t type = T_UNDEF; + return js_bool(nargs > 0 && util_is_boxed_primitive(args[0], &type) && type == T_BIGINT); +} + +static ant_value_t util_types_is_map_iterator(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || !is_object_type(args[0])) return js_false; + return js_bool(util_has_proto_in_chain(js, args[0], g_map_iter_proto)); +} + +static ant_value_t util_types_is_set_iterator(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1 || !is_object_type(args[0])) return js_false; + return js_bool(util_has_proto_in_chain(js, args[0], g_set_iter_proto)); +} + +static ant_value_t util_types_is_module_namespace_object(ant_t *js, ant_value_t *args, int nargs) { + if (nargs < 1) return js_false; + return js_bool(js_check_brand(args[0], BRAND_MODULE_NAMESPACE)); +} + +ant_value_t util_types_library(ant_t *js) { + ant_value_t types = js_mkobj(js); + + js_set(js, types, "isAnyArrayBuffer", js_mkfun(util_types_is_any_array_buffer)); + js_set(js, types, "isArrayBuffer", js_mkfun(util_types_is_array_buffer)); + js_set(js, types, "isArgumentsObject", js_mkfun(util_types_is_arguments_object)); + js_set(js, types, "isArrayBufferView", js_mkfun(util_types_is_array_buffer_view)); + js_set(js, types, "isAsyncFunction", js_mkfun(util_types_is_async_function)); + js_set(js, types, "isBigInt64Array", js_mkfun(util_types_is_bigint64_array)); + js_set(js, types, "isBigIntObject", js_mkfun(util_types_is_bigint_object)); + js_set(js, types, "isBigUint64Array", js_mkfun(util_types_is_biguint64_array)); + js_set(js, types, "isBooleanObject", js_mkfun(util_types_is_boolean_object)); + js_set(js, types, "isBoxedPrimitive", js_mkfun(util_types_is_boxed_primitive)); + js_set(js, types, "isDataView", js_mkfun(util_types_is_data_view)); + js_set(js, types, "isDate", js_mkfun(util_types_is_date)); + js_set(js, types, "isFloat16Array", js_mkfun(util_types_is_float16_array)); + js_set(js, types, "isFloat32Array", js_mkfun(util_types_is_float32_array)); + js_set(js, types, "isFloat64Array", js_mkfun(util_types_is_float64_array)); + js_set(js, types, "isGeneratorFunction", js_mkfun(util_types_is_generator_function)); + js_set(js, types, "isGeneratorObject", js_mkfun(util_types_is_generator_object)); + js_set(js, types, "isInt8Array", js_mkfun(util_types_is_int8_array)); + js_set(js, types, "isInt16Array", js_mkfun(util_types_is_int16_array)); + js_set(js, types, "isInt32Array", js_mkfun(util_types_is_int32_array)); + js_set(js, types, "isMap", js_mkfun(util_types_is_map)); + js_set(js, types, "isMapIterator", js_mkfun(util_types_is_map_iterator)); + js_set(js, types, "isModuleNamespaceObject", js_mkfun(util_types_is_module_namespace_object)); + js_set(js, types, "isNativeError", js_mkfun(util_types_is_native_error)); + js_set(js, types, "isNumberObject", js_mkfun(util_types_is_number_object)); + js_set(js, types, "isPromise", js_mkfun(util_types_is_promise)); + js_set(js, types, "isProxy", js_mkfun(util_types_is_proxy)); + js_set(js, types, "isRegExp", js_mkfun(util_types_is_regexp)); + js_set(js, types, "isSet", js_mkfun(util_types_is_set)); + js_set(js, types, "isSetIterator", js_mkfun(util_types_is_set_iterator)); + js_set(js, types, "isSharedArrayBuffer", js_mkfun(util_types_is_shared_array_buffer)); + js_set(js, types, "isStringObject", js_mkfun(util_types_is_string_object)); + js_set(js, types, "isSymbolObject", js_mkfun(util_types_is_symbol_object)); + js_set(js, types, "isTypedArray", js_mkfun(util_types_is_typed_array)); + js_set(js, types, "isUint8Array", js_mkfun(util_types_is_uint8_array)); + js_set(js, types, "isUint8ClampedArray", js_mkfun(util_types_is_uint8_clamped_array)); + js_set(js, types, "isUint16Array", js_mkfun(util_types_is_uint16_array)); + js_set(js, types, "isUint32Array", js_mkfun(util_types_is_uint32_array)); + js_set(js, types, "isWeakMap", js_mkfun(util_types_is_weak_map)); + js_set(js, types, "isWeakSet", js_mkfun(util_types_is_weak_set)); + + return types; +} + +static ant_value_t util_get_types_object(ant_t *js) { + bool loaded = false; + ant_value_t types = js_esm_load_registered_library(js, "util/types", 10, &loaded); + return loaded ? types : util_types_library(js); +} + static ant_value_t util_debuglog_call(ant_params_t) { return js_mkundef(); } @@ -645,8 +947,9 @@ static ant_value_t util_inherits(ant_t *js, ant_value_t *args, int nargs) { return js_mkundef(); } -ant_value_t util_library(ant_t *js) { +ant_value_t util_library(ant_t *js) { ant_value_t lib = js_mkobj(js); + ant_value_t types = util_get_types_object(js); js_set(js, lib, "format", js_mkfun(util_format)); js_set(js, lib, "formatWithOptions", js_mkfun(util_format_with_options)); @@ -658,6 +961,7 @@ ant_value_t util_library(ant_t *js) { js_set(js, lib, "promisify", js_mkfun(util_promisify)); js_set(js, lib, "stripVTControlCharacters", js_mkfun(util_strip_vt_control_characters)); js_set(js, lib, "styleText", js_mkfun(util_style_text)); + js_set(js, lib, "types", types); js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "util", 4)); return lib; diff --git a/src/modules/wasi.c b/src/modules/wasi.c index 090813a..b5c1865 100644 --- a/src/modules/wasi.c +++ b/src/modules/wasi.c @@ -35,6 +35,17 @@ typedef struct { wasm_function_inst_t func; } wasi_func_env_t; +static inline bool wasi_is_proc_exit_exception(const char *exception) { + return exception != NULL && strstr(exception, "wasi proc exit") != NULL; +} + +static ant_value_t wasi_handle_proc_exit(wasm_module_inst_t inst) { + uint32_t exit_code = wasm_runtime_get_wasi_exit_code(inst); + wasm_runtime_clear_exception(inst); + if (exit_code != 0) exit((int)exit_code); + return js_mkundef(); +} + static ant_value_t wasi_exported_func_call(ant_t *js, ant_value_t *args, int nargs) { if (!js_check_native_tag(js->current_func, WASI_FUNC_TAG)) return js_mkerr(js, "Invalid WASI function"); @@ -53,6 +64,7 @@ static ant_value_t wasi_exported_func_call(ant_t *js, ant_value_t *args, int nar if (!wasm_runtime_call_wasm(env->exec_env, env->func, param_count, wasm_argv)) { const char *exception = wasm_runtime_get_exception(env->inst); + if (wasi_is_proc_exit_exception(exception)) return wasi_handle_proc_exit(env->inst); return js_mkerr(js, "%s", exception ? exception : "WASI function call failed"); } diff --git a/src/types/modules/fs.d.ts b/src/types/modules/fs.d.ts index 9c718a4..3c7324d 100644 --- a/src/types/modules/fs.d.ts +++ b/src/types/modules/fs.d.ts @@ -1,4 +1,6 @@ declare module 'fs' { + import type { Readable, Writable } from 'stream'; + interface Stats { size: number; mode: number; @@ -31,6 +33,24 @@ declare module 'fs' { interval?: number; } + interface ReadStream extends Readable { + readonly path: string; + readonly bytesRead: number; + readonly pending: boolean; + readonly closed: boolean; + readonly fd?: number | null; + close(callback?: () => void): this; + } + + interface WriteStream extends Writable { + readonly path: string; + readonly bytesWritten: number; + readonly pending: boolean; + readonly closed: boolean; + readonly fd?: number | null; + close(callback?: () => void): this; + } + const constants: { F_OK: number; R_OK: number; @@ -50,9 +70,41 @@ declare module 'fs' { function readFile(path: string): Promise; function readFileSync(path: string, encoding: Encoding | { encoding: Encoding }): string; function readFileSync(path: string): Uint8Array; - function read(fd: number, buffer: ArrayBufferView, offset?: number, length?: number, position?: number | null, callback?: (err: Error | null, bytesRead: number, buffer: ArrayBufferView) => void): Promise; + function read( + fd: number, + buffer: ArrayBufferView, + offset?: number, + length?: number, + position?: number | null, + callback?: (err: Error | null, bytesRead: number, buffer: ArrayBufferView) => void + ): Promise; function readSync(fd: number, buffer: ArrayBufferView, offset?: number, length?: number, position?: number | null): number; function stream(path: string): Promise; + function createReadStream( + path: string, + options?: { + fd?: number; + flags?: string | number; + mode?: number; + start?: number; + end?: number; + autoClose?: boolean; + emitClose?: boolean; + highWaterMark?: number; + } + ): ReadStream; + function createWriteStream( + path: string, + options?: { + fd?: number; + flags?: string | number; + mode?: number; + start?: number; + autoClose?: boolean; + emitClose?: boolean; + highWaterMark?: number; + } + ): WriteStream; function open(path: string, flags?: string, mode?: number): Promise; function openSync(path: string, flags?: string, mode?: number): number; function close(fd: number): Promise; @@ -101,6 +153,37 @@ declare module 'fs' { const FSWatcher: { prototype: FSWatcher; }; + const ReadStream: { + prototype: ReadStream; + new ( + path: string, + options?: { + fd?: number; + flags?: string | number; + mode?: number; + start?: number; + end?: number; + autoClose?: boolean; + emitClose?: boolean; + highWaterMark?: number; + } + ): ReadStream; + }; + const WriteStream: { + prototype: WriteStream; + new ( + path: string, + options?: { + fd?: number; + flags?: string | number; + mode?: number; + start?: number; + autoClose?: boolean; + emitClose?: boolean; + highWaterMark?: number; + } + ): WriteStream; + }; } declare module 'ant:fs' { diff --git a/src/types/modules/stream.d.ts b/src/types/modules/stream.d.ts new file mode 100644 index 0000000..b34985f --- /dev/null +++ b/src/types/modules/stream.d.ts @@ -0,0 +1,77 @@ +declare module 'stream' { + class Stream { + pipe(destination: T, options?: { end?: boolean }): T; + unpipe(destination?: Writable): this; + pause(): this; + resume(): this; + isPaused(): boolean; + destroy(error?: Error): this; + on(event: string, listener: (...args: unknown[]) => void): this; + once(event: string, listener: (...args: unknown[]) => void): this; + off(event: string, listener: (...args: unknown[]) => void): this; + removeListener(event: string, listener: (...args: unknown[]) => void): this; + removeAllListeners(event?: string): this; + } + + class Readable extends Stream { + constructor(options?: Record); + _read(size?: number): void; + read(size?: number): Uint8Array | string | null; + push(chunk: Uint8Array | string | null): boolean; + on(event: 'data', listener: (chunk: Uint8Array | string) => void): this; + on(event: 'end' | 'close' | 'readable', listener: () => void): this; + on(event: 'error', listener: (error: Error) => void): this; + on(event: string, listener: (...args: unknown[]) => void): this; + static from(source: unknown, options?: Record): Readable; + static fromWeb(source: unknown, options?: Record): Readable; + } + + class Writable extends Stream { + constructor(options?: Record); + _write(chunk: Uint8Array | string, encoding: string, callback: (error?: Error | null) => void): void; + write(chunk: Uint8Array | string, encoding?: string, callback?: (error?: Error | null) => void): boolean; + end(chunk?: Uint8Array | string, encoding?: string, callback?: (error?: Error | null) => void): this; + cork(): void; + uncork(): void; + on(event: 'drain' | 'finish' | 'close', listener: () => void): this; + on(event: 'error', listener: (error: Error) => void): this; + on(event: string, listener: (...args: unknown[]) => void): this; + } + + class Duplex extends Readable { + constructor(options?: Record); + write(chunk: Uint8Array | string, encoding?: string, callback?: (error?: Error | null) => void): boolean; + end(chunk?: Uint8Array | string, encoding?: string, callback?: (error?: Error | null) => void): this; + cork(): void; + uncork(): void; + } + + class Transform extends Duplex { + constructor(options?: Record); + } + + class PassThrough extends Transform { + constructor(options?: Record); + } + + function pipeline(...streams: Array void)>): Stream; + function finished(stream: Stream, callback?: (error?: Error | null) => void): Stream; + + const promises: { + pipeline(...streams: Stream[]): Promise; + finished(stream: Stream): Promise; + }; + + export default Stream; + export { Stream, Readable, Writable, Duplex, Transform, PassThrough, pipeline, finished, promises }; +} + +declare module 'ant:stream' { + export * from 'stream'; + export { default } from 'stream'; +} + +declare module 'node:stream' { + export * from 'stream'; + export { default } from 'stream'; +} diff --git a/src/types/modules/timers.d.ts b/src/types/modules/timers.d.ts new file mode 100644 index 0000000..17a9f26 --- /dev/null +++ b/src/types/modules/timers.d.ts @@ -0,0 +1,17 @@ +declare module 'timers' { + export const setTimeout: typeof globalThis.setTimeout; + export const clearTimeout: typeof globalThis.clearTimeout; + export const setInterval: typeof globalThis.setInterval; + export const clearInterval: typeof globalThis.clearInterval; + export const setImmediate: typeof globalThis.setImmediate; + export const clearImmediate: typeof globalThis.clearImmediate; + export const queueMicrotask: typeof globalThis.queueMicrotask; +} + +declare module 'ant:timers' { + export * from 'timers'; +} + +declare module 'node:timers' { + export * from 'timers'; +} diff --git a/src/types/modules/util.d.ts b/src/types/modules/util.d.ts index f1146e2..9ca4376 100644 --- a/src/types/modules/util.d.ts +++ b/src/types/modules/util.d.ts @@ -1,3 +1,46 @@ +declare namespace utilTypes { + function isAnyArrayBuffer(value: unknown): boolean; + function isArrayBuffer(value: unknown): boolean; + function isArgumentsObject(value: unknown): boolean; + function isArrayBufferView(value: unknown): boolean; + function isAsyncFunction(value: unknown): boolean; + function isBigInt64Array(value: unknown): boolean; + function isBigIntObject(value: unknown): boolean; + function isBigUint64Array(value: unknown): boolean; + function isBooleanObject(value: unknown): boolean; + function isBoxedPrimitive(value: unknown): boolean; + function isDataView(value: unknown): boolean; + function isDate(value: unknown): boolean; + function isFloat16Array(value: unknown): boolean; + function isFloat32Array(value: unknown): boolean; + function isFloat64Array(value: unknown): boolean; + function isGeneratorFunction(value: unknown): boolean; + function isGeneratorObject(value: unknown): boolean; + function isInt8Array(value: unknown): boolean; + function isInt16Array(value: unknown): boolean; + function isInt32Array(value: unknown): boolean; + function isMap(value: unknown): boolean; + function isMapIterator(value: unknown): boolean; + function isModuleNamespaceObject(value: unknown): boolean; + function isNativeError(value: unknown): boolean; + function isNumberObject(value: unknown): boolean; + function isPromise(value: unknown): boolean; + function isProxy(value: unknown): boolean; + function isRegExp(value: unknown): boolean; + function isSet(value: unknown): boolean; + function isSetIterator(value: unknown): boolean; + function isSharedArrayBuffer(value: unknown): boolean; + function isStringObject(value: unknown): boolean; + function isSymbolObject(value: unknown): boolean; + function isTypedArray(value: unknown): boolean; + function isUint8Array(value: unknown): boolean; + function isUint8ClampedArray(value: unknown): boolean; + function isUint16Array(value: unknown): boolean; + function isUint32Array(value: unknown): boolean; + function isWeakMap(value: unknown): boolean; + function isWeakSet(value: unknown): boolean; +} + declare module 'util' { type StyleTextFormat = string | string[]; @@ -14,6 +57,8 @@ declare module 'util' { function promisify(fn: (...args: unknown[]) => unknown): (...args: unknown[]) => Promise; function stripVTControlCharacters(str: string): string; function styleText(format: StyleTextFormat, text: string, options?: StyleTextOptions): string; + + const types: typeof utilTypes; } declare module 'ant:util' { @@ -23,3 +68,11 @@ declare module 'ant:util' { declare module 'node:util' { export * from 'util'; } + +declare module 'util/types' { + export = utilTypes; +} + +declare module 'node:util/types' { + export = utilTypes; +} diff --git a/tests/test_fs_streams.cjs b/tests/test_fs_streams.cjs new file mode 100644 index 0000000..531de39 --- /dev/null +++ b/tests/test_fs_streams.cjs @@ -0,0 +1,53 @@ +const fs = require('node:fs'); +const sourcePath = 'tests/.fs_stream_source.txt'; +const copyPath = 'tests/.fs_stream_copy.txt'; +const content = 'hello from fs streams\nline two'; + +try { + fs.unlinkSync(sourcePath); +} catch {} +try { + fs.unlinkSync(copyPath); +} catch {} + +function fail(error) { + console.error(error); + process.exit(1); +} + +const writer = fs.createWriteStream(sourcePath); +if (!(writer instanceof fs.WriteStream)) throw new Error('createWriteStream() did not return fs.WriteStream'); +writer.on('error', fail); +writer.on('finish', () => { + const written = fs.readFileSync(sourcePath, 'utf8'); + if (written !== content) fail(new Error(`unexpected write content: ${written}`)); + + const reader = fs.createReadStream(sourcePath); + if (!(reader instanceof fs.ReadStream)) fail(new Error('createReadStream() did not return fs.ReadStream')); + + let readBack = ''; + reader.on('error', fail); + reader.on('data', chunk => { + readBack += chunk.toString(); + }); + reader.on('end', () => { + if (readBack !== content) fail(new Error(`unexpected read content: ${readBack}`)); + + const pipedReader = fs.createReadStream(sourcePath); + const pipedWriter = fs.createWriteStream(copyPath); + pipedReader.on('error', fail); + pipedWriter.on('error', fail); + pipedWriter.on('finish', () => { + const copied = fs.readFileSync(copyPath, 'utf8'); + if (copied !== content) fail(new Error(`unexpected piped content: ${copied}`)); + + fs.unlinkSync(sourcePath); + fs.unlinkSync(copyPath); + console.log('fs stream test passed'); + }); + pipedReader.pipe(pipedWriter); + }); +}); + +writer.write('hello '); +writer.end('from fs streams\nline two');