Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ _100,000번 반복 × 10,000 동시 실행 · 1 KB 페이로드 · 10회 측정_
### <img src="https://github.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/blob/master/Emojis/Travel%20and%20places/High%20Voltage.png?raw=true" alt="High Voltage" width="25" height="25" /> 성능

- `setImmediate` 기반 파이프라인 자동 병합
- Zero-copy RESP 파서 (버퍼 슬라이스 재사용)
- 바이너리 세이프 RESP 파서와 독립 버퍼 반환
- 백프레셔 대응 청크 단위 소켓 쓰기
- 이벤트 루프 양보 포인트 설정 가능

Expand Down Expand Up @@ -385,7 +385,7 @@ graph TD
direction LR
Conn[Connection<br/><sub>TCP · TLS · Reconnect</sub>]
Req[Requester<br/><sub>Queue · Pipeline · Timeout</sub>]
Parse[Parser<br/><sub>RESP2 · RESP3 · Zero-copy</sub>]
Parse[Parser<br/><sub>RESP2 · RESP3 · Binary-safe</sub>]
PS[PubSub<br/><sub>Channel · Pattern · Shard</sub>]
DM[Debug Memory<br/><sub>Ring buffer · Sanitized</sub>]
end
Expand Down Expand Up @@ -419,13 +419,13 @@ sequenceDiagram
Client-->>App: 'OK'
```

| 모듈 | 역할 |
| :--------------- | :---------------------------------------------- |
| **Connection** | TCP/TLS 소켓 관리, 재연결 백오프 |
| **Requester** | 커맨드 큐, 파이프라인 청킹, 응답 매칭, 타임아웃 |
| **Parser** | RESP 디코딩, 버퍼 관리, zero-copy 슬라이싱 |
| **PubSub** | 채널/패턴/샤드 상태 추적, 메시지 디스패치 |
| **Debug Memory** | 링 버퍼 기반 디버그 로그, credential 마스킹 |
| 모듈 | 역할 |
| :--------------- | :------------------------------------------------ |
| **Connection** | TCP/TLS 소켓 관리, 재연결 백오프 |
| **Requester** | 커맨드 큐, 파이프라인 청킹, 응답 매칭, 타임아웃 |
| **Parser** | RESP 디코딩, 버퍼 관리, 바이너리 세이프 응답 처리 |
| **PubSub** | 채널/패턴/샤드 상태 추적, 메시지 디스패치 |
| **Debug Memory** | 링 버퍼 기반 디버그 로그, credential 마스킹 |

## 이벤트

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ _100,000 iterations × 10,000 concurrency · 1 KB payload · 10 repeats_
### <img src="https://github.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/blob/master/Emojis/Travel%20and%20places/High%20Voltage.png?raw=true" alt="High Voltage" width="25" height="25" /> Performance

- `setImmediate` pipeline coalescing
- Zero-copy RESP parser (borrowed buffer slices)
- Binary-safe RESP parser with owned buffers
- Chunked socket writes with backpressure
- Configurable event-loop yield points

Expand Down Expand Up @@ -385,7 +385,7 @@ graph TD
direction LR
Conn[Connection<br/><sub>TCP · TLS · Reconnect</sub>]
Req[Requester<br/><sub>Queue · Pipeline · Timeout</sub>]
Parse[Parser<br/><sub>RESP2 · RESP3 · Zero-copy</sub>]
Parse[Parser<br/><sub>RESP2 · RESP3 · Binary-safe</sub>]
PS[PubSub<br/><sub>Channel · Pattern · Shard</sub>]
DM[Debug Memory<br/><sub>Ring buffer · Sanitized</sub>]
end
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vcms-io/solidis",
"version": "0.3.0",
"version": "0.3.1",
"author": "Jay Lee <jay@vendit.co.kr>",
"description": "The fastest Redis client for Node.js. Zero dependencies, 2x+ faster than ioredis, battle-tested in production.",
"repository": {
Expand Down
53 changes: 53 additions & 0 deletions scripts/tests/commands/0028.parser-edge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,59 @@ describe('parser-edge', () => {
});

describe('internal buffer management', () => {
it('returns bulk buffers that are independent from the input chunk', async () => {
const parser = createParser();
const chunk = bytes('$5\r\nhello\r\n');

const [reply] = await parser.queueParse(chunk);

chunk.fill(0);

assert.deepStrictEqual(reply, bytes('hello'));
});

it('keeps a returned bulk buffer stable after caller mutation and later parses', async () => {
const parser = createParser();

const [first] = await parser.queueParse(bytes('$3\r\nabc\r\n'));

if (!Buffer.isBuffer(first)) {
assert.fail('expected first reply to be a Buffer');
}

first.fill(0);

const [second] = await parser.queueParse(bytes('$3\r\nxyz\r\n'));

assert.deepStrictEqual(first, Buffer.alloc(3));
assert.deepStrictEqual(second, bytes('xyz'));
});

it('does not mutate a held bulk buffer when later replies shift the internal buffer', async () => {
const parser = new SolidisParser({
...SolidisDefaultOptions,
parser: {
buffer: { initial: 512, shiftThreshold: 16 },
maxBulkStringLength: 1048576,
},
});

const payload = 'x'.repeat(256);
const [bulk] = await parser.queueParse(
bytes(`$${payload.length}\r\n${payload}\r\n`),
);

if (!Buffer.isBuffer(bulk)) {
assert.fail('expected bulk reply to be a Buffer');
}

const snapshot = Buffer.from(bulk);

await parser.queueParse(bytes('+OK\r\n'.repeat(30)));

assert.deepStrictEqual(bulk, snapshot);
});

it('grows the internal buffer when a second chunk exceeds remaining capacity', async () => {
const parser = new SolidisParser({
...SolidisDefaultOptions,
Expand Down
46 changes: 1 addition & 45 deletions sources/modules/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import type {

export class SolidisParser {
#buffer: Buffer;
#bufferHasBorrowedSlices = false;
#bufferIsExternal = false;
#initialBufferSize: number;
#shiftThreshold: number;
#maxBulkStringLength: number;
Expand Down Expand Up @@ -76,20 +74,6 @@ export class SolidisParser {
}

#appendBuffer(parseBuffer: Buffer) {
if (this.#readOffset === this.#writeOffset) {
this.#buffer = parseBuffer;
this.#bufferHasBorrowedSlices = false;
this.#bufferIsExternal = true;
this.#readOffset = 0;
this.#writeOffset = parseBuffer.length;

return;
}

if (this.#bufferIsExternal) {
this.#moveUnreadBytesToInternalBuffer(parseBuffer.length);
}

if (this.#writeOffset + parseBuffer.length > this.#buffer.length) {
this.#growInternalBuffer(this.#writeOffset + parseBuffer.length);
}
Expand All @@ -99,20 +83,6 @@ export class SolidisParser {
this.#writeOffset += parseBuffer.length;
}

#moveUnreadBytesToInternalBuffer(additionalCapacity: number) {
const remainingBytes = this.#writeOffset - this.#readOffset;
const minCapacity = remainingBytes + additionalCapacity;
const newBuffer = this.#allocateInternalBuffer(minCapacity);

this.#buffer.copy(newBuffer, 0, this.#readOffset, this.#writeOffset);

this.#buffer = newBuffer;
this.#bufferHasBorrowedSlices = false;
this.#bufferIsExternal = false;
this.#readOffset = 0;
this.#writeOffset = remainingBytes;
}

#allocateInternalBuffer(minCapacity: number) {
let newCapacity = Math.max(this.#initialBufferSize, this.#buffer.length);

Expand All @@ -132,8 +102,6 @@ export class SolidisParser {
this.#readOffset = 0;

this.#buffer = newBuffer;
this.#bufferHasBorrowedSlices = false;
this.#bufferIsExternal = false;
}

#tryShiftInternalBuffer() {
Expand All @@ -144,10 +112,6 @@ export class SolidisParser {
return;
}

if (this.#bufferIsExternal || this.#bufferHasBorrowedSlices) {
return;
}

if (
this.#readOffset > this.#shiftThreshold &&
this.#readOffset < this.#writeOffset
Expand Down Expand Up @@ -360,7 +324,7 @@ export class SolidisParser {
}

default: {
data = this.#borrowParsedBuffer(parsed.data);
data = Buffer.from(parsed.data);
break;
}
}
Expand All @@ -372,14 +336,6 @@ export class SolidisParser {
};
}

#borrowParsedBuffer(buffer: Buffer) {
if (!this.#bufferIsExternal) {
this.#bufferHasBorrowedSlices = true;
}

return buffer;
}

#parseSimpleLine(type: SolidisRespSimpleLineType): SolidisParsed {
const parsed = this.#parseLine(this.#readOffset + 1, type);

Expand Down
2 changes: 1 addition & 1 deletion website/components/architecture-diagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export function ArchitectureDiagram({
width={parserWidth}
height={parserHeight}
title="Parser"
subtitle="RESP2 · RESP3 · Zero-copy"
subtitle="RESP2 · RESP3 · Binary-safe"
/>
<DiagramNode
x={pubsubX}
Expand Down