()
| 174 | } |
| 175 | |
| 176 | async processBuffer() { |
| 177 | const redis = getRedisCache(); |
| 178 | |
| 179 | const lrangeStart = performance.now(); |
| 180 | const queueEvents = await redis.lrange( |
| 181 | this.queueKey, |
| 182 | 0, |
| 183 | this.batchSize - 1 |
| 184 | ); |
| 185 | const lrangeMs = performance.now() - lrangeStart; |
| 186 | |
| 187 | if (queueEvents.length === 0) { |
| 188 | this.reportFlushStats({ rowsProcessed: 0, phases: { lrangeMs } }); |
| 189 | return; |
| 190 | } |
| 191 | |
| 192 | // We don't need to JSON.parse the events at all — they're already |
| 193 | // valid JSONEachRow lines (one stringified event per Redis entry). |
| 194 | // The client's custom `json.stringify` (set in CLICKHOUSE_OPTIONS) |
| 195 | // passes strings through unchanged, so the bytes go straight from |
| 196 | // Redis → CH HTTP body. This skips: |
| 197 | // - JSON.parse × N (50–300ms for N=100k) |
| 198 | // - The @clickhouse/client's internal JSON.stringify × N (same) |
| 199 | // - All the intermediate object allocations (saves ~200MB heap) |
| 200 | // |
| 201 | // We still need `project_id` per row for the per-project pub/sub. |
| 202 | // extractProjectId() does an indexOf-based fast path that's ~50× |
| 203 | // faster than JSON.parse, and falls back to a real parse on the |
| 204 | // rare line where `project_id` appears more than once (e.g. a |
| 205 | // user-supplied `properties.project_id`) — so the count is always |
| 206 | // attributed to the top-level field, never a nested one. |
| 207 | const countByProject = new Map<string, number>(); |
| 208 | const yieldEvery = this.getYieldInterval(queueEvents.length, { |
| 209 | min: 1000, |
| 210 | max: 5000, |
| 211 | }); |
| 212 | for (let i = 0; i < queueEvents.length; i++) { |
| 213 | const projectId = extractProjectId(queueEvents[i]!); |
| 214 | if (projectId) { |
| 215 | countByProject.set( |
| 216 | projectId, |
| 217 | (countByProject.get(projectId) ?? 0) + 1, |
| 218 | ); |
| 219 | } |
| 220 | if ((i + 1) % yieldEvery === 0) { |
| 221 | await this.yieldToEventLoop(); |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | const chStart = performance.now(); |
| 226 | await this.parallelLimit( |
| 227 | this.chunks(queueEvents, this.chunkSize), |
| 228 | (chunk) => |
| 229 | ch.insert({ |
| 230 | table: 'events', |
| 231 | // Stream the raw JSONEachRow lines straight through — already |
| 232 | // serialized in Redis, no client-side parse/stringify needed. |
| 233 | values: this.jsonEachRowStream(chunk), |
no test coverage detected