UNPKG

@ydbjs/topic

Version:

YDB Topics client for publish-subscribe messaging. Provides at-least-once delivery, exactly-once publishing, FIFO guarantees, and scalable message processing for unstructured data.

225 lines 12.4 kB
import { abortable } from "@ydbjs/abortable"; import { Codec } from "@ydbjs/api/topic"; import { timestampMs } from "@bufbuild/protobuf/wkt"; import { loggers } from "@ydbjs/debug"; import { TopicMessage } from "../message.js"; import { _send_read_request } from "./_read_request.js"; let dbg = loggers.topic.extend('reader'); export let _read = function read(ctx, options = {}) { let limit = options.limit || Infinity; let signal = options.signal; let waitMs = options.waitMs || 60_000; dbg.log('starting read operation with limit=%s, waitMs=%d, hasSignal=%s', limit === Infinity ? 'unlimited' : limit, waitMs, !!signal); dbg.log('reader state: disposed=%s, bufferSize=%d, freeBufferSize=%d, partitionSessions=%d', ctx.disposed, ctx.buffer.length, ctx.freeBufferSize, ctx.partitionSessions.size); // Check if the reader has been disposed, cannot read with disposed reader if (ctx.disposed) { throw new Error('Reader is disposed'); } // Merge the provided signal with the reader's controller signal. if (signal) { signal = AbortSignal.any([ctx.controller.signal, signal]); } else { signal = ctx.controller.signal; } // If the signal is already aborted, throw an error immediately. if (signal.aborted) { throw new Error('Read aborted', { cause: signal.reason }); } return (async function* () { let messageCount = 0; while (true) { dbg.log('generator iteration called, messageCount=%d, limit=%s', messageCount, limit === Infinity ? 'unlimited' : limit); // If the reader is disposed, return if (ctx.disposed) { dbg.log('reader disposed during iteration, returning'); return; } // If the signal is already aborted, return if (signal.aborted) { dbg.log('signal aborted during iteration, returning'); return; } // If we've reached the limit, return if (messageCount >= limit) { dbg.log('limit reached, returning'); return; } let messages = []; // Wait for the next readResponse or until the timeout expires. if (!ctx.buffer.length) { dbg.log('buffer empty, waiting for data (waitMs=%d)', waitMs); let waiter = Promise.withResolvers(); // Wait for new data to arrive let bufferCheckInterval = setInterval(() => { if (ctx.buffer.length > 0) { dbg.log('data arrived in buffer, resolving waiter (bufferSize=%d)', ctx.buffer.length); waiter.resolve(undefined); } }, 10); // Check every 10ms try { // oxlint-disable-next-line no-await-in-loop await abortable(AbortSignal.any([signal, AbortSignal.timeout(waitMs)]), waiter.promise) .finally(() => { clearInterval(bufferCheckInterval); }); } catch { clearInterval(bufferCheckInterval); if (signal.aborted) { dbg.log('read aborted during wait, finishing'); return; } dbg.log('wait timeout expired, yielding empty result'); yield []; continue; } if (signal.aborted) { dbg.log('read aborted during wait, finishing'); return; } if (ctx.disposed) { dbg.log('reader disposed during wait, finishing'); return; } } let releasableBufferBytes = 0n; while (ctx.buffer.length && messageCount < limit) { let fullRead = true; let response = ctx.buffer.shift(); // Get the first response from the buffer if (response.partitionData.length === 0) { dbg.log('skipping empty response'); continue; // Skip empty responses } // If we have a limit and reached it, break the loop if (messageCount >= limit) { ctx.buffer.unshift(response); // Put the response back to the front of the buffer break; } while (response.partitionData.length && messageCount < limit) { let pd = response.partitionData.shift(); // Get the first partition data if (pd.batches.length === 0) { dbg.log('skipping empty partition data for sessionId=%s', pd.partitionSessionId); continue; // Skip empty partition data } // If we have a limit and reached it, break the loop if (messageCount >= limit) { response.partitionData.unshift(pd); // Put the partition data back to the front of the response break; } while (pd.batches.length && messageCount < limit) { let batch = pd.batches.shift(); // Get the first batch if (batch.messageData.length === 0) { dbg.log('skipping empty batch from producer=%s', batch.producerId); continue; // Skip empty batches } // If we have a limit and reached it, break the loop if (messageCount >= limit) { pd.batches.unshift(batch); // Put the batch back to the front of the partition data break; } let partitionSession = ctx.partitionSessions.get(pd.partitionSessionId); if (!partitionSession) { dbg.log('error: readResponse for unknown partitionSessionId=%s', pd.partitionSessionId); continue; } if (partitionSession.isStopped) { dbg.log('error: readResponse for stopped partitionSessionId=%s', pd.partitionSessionId); continue; } while (batch.messageData.length && messageCount < limit) { // Process each message in the batch let msg = batch.messageData.shift(); // Get the first message from the batch // If we have a limit and reached it, break the loop if (messageCount >= limit) { batch.messageData.unshift(msg); // Put the message back to the front of the batch break; } let payload = msg.data; if (batch.codec !== Codec.UNSPECIFIED) { if (!ctx.codecs.has(batch.codec)) { dbg.log('error: codec %s is not supported', batch.codec); throw new Error(`Codec ${batch.codec} is not supported`); } // Decompress the message data using the provided decompress function try { payload = ctx.codecs.get(batch.codec).decompress(msg.data); } catch (error) { dbg.log('error: failed to decompress message data: %O', error); throw error; } } // Process the message let message = new TopicMessage({ partitionSession: partitionSession, producer: batch.producerId, payload: payload, codec: batch.codec, seqNo: msg.seqNo, offset: msg.offset, uncompressedSize: msg.uncompressedSize, ...(msg.createdAt && { createdAt: timestampMs(msg.createdAt) }), ...(batch.writtenAt && { writtenAt: timestampMs(batch.writtenAt) }), ...(msg.metadataItems && { metadataItems: Object.fromEntries(msg.metadataItems.map(item => [item.key, item.value])) }) }); // Track read offset for transaction support if (ctx.readOffsets) { let existing = ctx.readOffsets.get(pd.partitionSessionId); if (existing) { // Update last offset, keep first offset existing.lastOffset = msg.offset; } else { // First message for this partition session ctx.readOffsets.set(pd.partitionSessionId, { firstOffset: msg.offset, lastOffset: msg.offset }); } } messages.push(message); messageCount++; } if (batch.messageData.length != 0) { fullRead = false; pd.batches.unshift(batch); // Put the batch back to the front of the partition data } } if (pd.batches.length != 0) { fullRead = false; response.partitionData.unshift(pd); // Put the partition data back to the front of the response } } if (response.partitionData.length != 0) { fullRead = false; ctx.buffer.unshift(response); // Put the response back to the front of the buffer } // If we have read all messages from the response, we can release its buffer allocation if (response.partitionData.length === 0 && fullRead) { releasableBufferBytes += response.bytesSize; dbg.log('response fully processed, releasing %s bytes from buffer', response.bytesSize); } } dbg.log('message processing complete: yielding %d messages, total messageCount=%d', messages.length, messageCount); dbg.log('buffer state: bufferSize=%d, maxBufferSize=%d, freeBufferSize=%d, releasableBytes=%s', ctx.buffer.length, ctx.maxBufferSize, ctx.freeBufferSize, releasableBufferBytes); dbg.log('yield %d messages, buffer size is %d bytes, free buffer size is %d bytes', messages.length, ctx.maxBufferSize - ctx.freeBufferSize, ctx.freeBufferSize); if (releasableBufferBytes > 0n) { // Update free buffer size using helper function ctx.updateFreeBufferSize(releasableBufferBytes); // If we have free buffer space, request more data. _send_read_request({ queue: ctx.outgoingQueue, bytesSize: releasableBufferBytes }); } dbg.log('generator yielding: messagesCount=%d', messages.length); yield messages; // If we've reached the limit or no messages were yielded and buffer is empty, return if (messageCount >= limit || (messages.length === 0 && !ctx.buffer.length)) { return; } } })(); }; //# sourceMappingURL=_read.js.map