@linebridge/engine-neo 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/response.d.ts CHANGED
@@ -43,6 +43,10 @@ export default class Response<TServer = Server<any>> implements BaseHttpResponse
43
43
  _status_code: number;
44
44
  /** Custom status message (overrides the default from STATUS_CODES). */
45
45
  _status_message: string | null;
46
+ /** Cached engine reference to avoid walking route.server on every response. */
47
+ _engine: EngineAdaptor | null;
48
+ /** Whether at least one header was queued. Avoids enumerating an empty object. */
49
+ _has_headers: boolean;
46
50
  /** Response headers (key → value or key → string[]). */
47
51
  _headers: Record<string, string | string[]>;
48
52
  /** Set-Cookie headers (name → serialized cookie string). */
@@ -141,11 +145,16 @@ export default class Response<TServer = Server<any>> implements BaseHttpResponse
141
145
  * Must return `true` if the chunk was successfully written.
142
146
  */
143
147
  drain(handler: (offset: number) => boolean): void;
148
+ /**
149
+ * Marks the response as completed and fires the lifecycle listeners.
150
+ * Used by every path that finishes a response, including streaming.
151
+ */
152
+ _mark_completed(): void;
144
153
  /**
145
154
  * Sends the response body and marks the response as completed.
146
155
  *
147
- * Supports corked (batched) and uncorked paths, streaming mode, and
148
- * `endWithoutBody` when a custom `content-length` header is set.
156
+ * All uWS writes are wrapped in a single cork callback so status,
157
+ * headers and body are flushed atomically.
149
158
  *
150
159
  * Fires `finish` and `close` event listeners.
151
160
  */
@@ -178,7 +187,7 @@ export default class Response<TServer = Server<any>> implements BaseHttpResponse
178
187
  redirect(url: string): boolean;
179
188
  /**
180
189
  * Fast-path send used by the `_invokeHandler`.
181
- * Skips cork/uncork logic and writes directly.
190
+ * Corks the writes so status, headers and body are flushed atomically.
182
191
  */
183
192
  _sendFast(body: any): void;
184
193
  /**
package/lib/response.js CHANGED
@@ -46,6 +46,10 @@ export default class Response {
46
46
  _status_code;
47
47
  /** Custom status message (overrides the default from STATUS_CODES). */
48
48
  _status_message;
49
+ /** Cached engine reference to avoid walking route.server on every response. */
50
+ _engine;
51
+ /** Whether at least one header was queued. Avoids enumerating an empty object. */
52
+ _has_headers;
49
53
  /** Response headers (key → value or key → string[]). */
50
54
  _headers;
51
55
  /** Set-Cookie headers (name → serialized cookie string). */
@@ -79,26 +83,36 @@ export default class Response {
79
83
  const res = new Response();
80
84
  res._raw_response = raw_response;
81
85
  res.route = route;
86
+ res._engine = route.engine ?? null;
82
87
  res._wrapped_request = request;
83
88
  res._upgrade_socket = socket || null;
84
- res._headers = {};
89
+ res._headers = Object.create(null);
90
+ res._has_headers = false;
91
+ res._cookies = null;
92
+ res._events = null;
93
+ res._streaming = false;
94
+ res._cork = false;
95
+ res._corked = false;
96
+ res._drain_handler = null;
85
97
  res._status_code = 200;
98
+ res._status_message = null;
86
99
  res._middleware_cursor = -1;
87
100
  // handle uWS abort (client disconnects before response is sent)
88
101
  raw_response.onAborted(() => {
89
102
  if (res.completed)
90
103
  return;
91
104
  res.completed = true;
92
- res.route?.server.engine._resolve_pending_request();
105
+ res._engine?._resolve_pending_request();
93
106
  //res._wrapped_request.pause()
94
- if (res._events?.abort) {
95
- for (let i = 0; i < res._events.abort.length; i++) {
96
- res._events.abort[i](res._wrapped_request, res);
107
+ const events = res._events;
108
+ if (events?.abort) {
109
+ for (let i = 0; i < events.abort.length; i++) {
110
+ events.abort[i](res._wrapped_request, res);
97
111
  }
98
112
  }
99
- if (res._events?.close) {
100
- for (let i = 0; i < res._events.close.length; i++) {
101
- res._events.close[i](res._wrapped_request, res);
113
+ if (events?.close) {
114
+ for (let i = 0; i < events.close.length; i++) {
115
+ events.close[i](res._wrapped_request, res);
102
116
  }
103
117
  }
104
118
  });
@@ -187,6 +201,7 @@ export default class Response {
187
201
  }
188
202
  this._headers["content-type"] =
189
203
  mime_types.contentType(mime_type) || "text/plain";
204
+ this._has_headers = true;
190
205
  return this;
191
206
  }
192
207
  /**
@@ -197,6 +212,7 @@ export default class Response {
197
212
  name = name.toLowerCase();
198
213
  if (overwrite || this._headers[name] === undefined) {
199
214
  this._headers[name] = value;
215
+ this._has_headers = true;
200
216
  return this;
201
217
  }
202
218
  const existing = this._headers[name];
@@ -269,7 +285,7 @@ export default class Response {
269
285
  // perform the uWS upgrade with the stored socket handle
270
286
  this._raw_response.upgrade({ context }, headers["sec-websocket-key"], headers["sec-websocket-protocol"], headers["sec-websocket-extensions"], this._upgrade_socket);
271
287
  this.completed = true;
272
- this.engine?._resolve_pending_request();
288
+ this._engine?._resolve_pending_request();
273
289
  }
274
290
  /**
275
291
  * Writes the status line and all accumulated headers to the uWS response.
@@ -281,31 +297,34 @@ export default class Response {
281
297
  return false;
282
298
  this.initiated = true;
283
299
  const raw = this._raw_response;
284
- if (this._status_message) {
285
- raw.writeStatus(`${this._status_code} ${this._status_message}`);
286
- }
287
- else {
288
- raw.writeStatus(STATUS_CACHE[this._status_code] || `${this._status_code} OK`);
289
- }
290
- const headerKeys = Object.keys(this._headers);
291
- for (let i = 0; i < headerKeys.length; i++) {
292
- const name = headerKeys[i];
293
- if (name === "content-length")
294
- continue;
295
- const values = this._headers[name];
296
- if (Array.isArray(values)) {
297
- for (let j = 0; j < values.length; j++) {
298
- raw.writeHeader(name, values[j]);
300
+ const status_code = this._status_code;
301
+ const status_message = this._status_message;
302
+ if (status_message) {
303
+ raw.writeStatus(`${status_code} ${status_message}`);
304
+ }
305
+ else if (status_code !== 200) {
306
+ raw.writeStatus(STATUS_CACHE[status_code] || `${status_code} OK`);
307
+ }
308
+ if (this._has_headers) {
309
+ const headers = this._headers;
310
+ for (const name in headers) {
311
+ if (name === "content-length")
312
+ continue;
313
+ const values = headers[name];
314
+ if (Array.isArray(values)) {
315
+ for (let j = 0; j < values.length; j++) {
316
+ raw.writeHeader(name, values[j]);
317
+ }
318
+ }
319
+ else {
320
+ raw.writeHeader(name, values);
299
321
  }
300
- }
301
- else {
302
- raw.writeHeader(name, values);
303
322
  }
304
323
  }
305
324
  if (this._cookies) {
306
- const cookieKeys = Object.keys(this._cookies);
307
- for (let i = 0; i < cookieKeys.length; i++) {
308
- raw.writeHeader("set-cookie", this._cookies[cookieKeys[i]]);
325
+ const cookies = this._cookies;
326
+ for (const name in cookies) {
327
+ raw.writeHeader("set-cookie", cookies[name]);
309
328
  }
310
329
  }
311
330
  return true;
@@ -329,71 +348,64 @@ export default class Response {
329
348
  });
330
349
  }
331
350
  }
351
+ /**
352
+ * Marks the response as completed and fires the lifecycle listeners.
353
+ * Used by every path that finishes a response, including streaming.
354
+ */
355
+ _mark_completed() {
356
+ if (this.completed)
357
+ return;
358
+ const events = this._events;
359
+ if (!events) {
360
+ this.completed = true;
361
+ this._engine?._resolve_pending_request();
362
+ return;
363
+ }
364
+ const finish = events.finish;
365
+ if (!this._streaming && finish) {
366
+ for (let i = 0; i < finish.length; i++) {
367
+ finish[i](this._wrapped_request, this);
368
+ }
369
+ }
370
+ this.completed = true;
371
+ this._engine?._resolve_pending_request();
372
+ const close = events.close;
373
+ if (close) {
374
+ for (let i = 0; i < close.length; i++) {
375
+ close[i](this._wrapped_request, this);
376
+ }
377
+ }
378
+ }
332
379
  /**
333
380
  * Sends the response body and marks the response as completed.
334
381
  *
335
- * Supports corked (batched) and uncorked paths, streaming mode, and
336
- * `endWithoutBody` when a custom `content-length` header is set.
382
+ * All uWS writes are wrapped in a single cork callback so status,
383
+ * headers and body are flushed atomically.
337
384
  *
338
385
  * Fires `finish` and `close` event listeners.
339
386
  */
340
387
  send(body, close_connection) {
341
388
  if (this.completed)
342
389
  return this;
343
- if (this._cork && !this._corked) {
344
- this._corked = true;
345
- this._raw_response.cork(() => {
346
- this._initiate_response();
347
- if (body !== undefined || this._streaming) {
348
- this._raw_response.end(body, close_connection);
349
- }
350
- else {
351
- const custom_length = this._headers["content-length"];
352
- if (custom_length) {
353
- const content_length = typeof custom_length === "string"
354
- ? custom_length
355
- : custom_length[custom_length.length - 1];
356
- this._raw_response.endWithoutBody(content_length, close_connection);
357
- }
358
- else {
359
- this._raw_response.end(body, close_connection);
360
- }
361
- }
362
- });
363
- }
364
- else {
390
+ const raw = this._raw_response;
391
+ raw.cork(() => {
365
392
  this._initiate_response();
366
- const raw = this._raw_response;
367
393
  if (body !== undefined || this._streaming) {
368
394
  raw.end(body, close_connection);
395
+ return;
369
396
  }
370
- else {
371
- const custom_length = this._headers["content-length"];
372
- if (custom_length) {
373
- const content_length = typeof custom_length === "string"
374
- ? custom_length
375
- : custom_length[custom_length.length - 1];
376
- raw.endWithoutBody(content_length, close_connection);
377
- }
378
- else {
379
- raw.end(body, close_connection);
380
- }
381
- }
382
- }
383
- if (!this._streaming && this.listenerCount("finish") > 0) {
384
- const handlers = this._events.finish;
385
- for (let i = 0; i < handlers.length; i++) {
386
- handlers[i](this._wrapped_request, this);
387
- }
388
- }
389
- this.completed = true;
390
- this.engine?._resolve_pending_request();
391
- if (this.listenerCount("close") > 0) {
392
- const handlers = this._events.close;
393
- for (let i = 0; i < handlers.length; i++) {
394
- handlers[i](this._wrapped_request, this);
397
+ const custom_length = this._headers["content-length"];
398
+ if (!custom_length) {
399
+ raw.end(body, close_connection);
400
+ return;
395
401
  }
396
- }
402
+ const content_length = typeof custom_length === "string"
403
+ ? custom_length
404
+ : custom_length[custom_length.length - 1];
405
+ raw.endWithoutBody(content_length, close_connection);
406
+ });
407
+ this._corked = true;
408
+ this._mark_completed();
397
409
  return this;
398
410
  }
399
411
  _uws_write_chunk(chunk, total_size) {
@@ -407,17 +419,16 @@ export default class Response {
407
419
  * Initiates headers if not yet sent. Returns false under backpressure.
408
420
  */
409
421
  write(chunk) {
410
- if (this._cork && !this._corked) {
411
- this._corked = true;
412
- let ok = false;
413
- this._raw_response.cork(() => {
414
- this._initiate_response();
415
- ok = this._raw_response.write(chunk);
416
- });
417
- return ok;
418
- }
419
- this._initiate_response();
420
- return this._raw_response.write(chunk);
422
+ if (this.completed)
423
+ return false;
424
+ const raw = this._raw_response;
425
+ let ok = false;
426
+ raw.cork(() => {
427
+ this._initiate_response();
428
+ ok = raw.write(chunk);
429
+ });
430
+ this._corked = true;
431
+ return ok;
421
432
  }
422
433
  /**
423
434
  * Streams a single chunk with backpressure support.
@@ -427,30 +438,44 @@ export default class Response {
427
438
  if (this.completed)
428
439
  return Promise.resolve();
429
440
  return new Promise((resolve) => {
430
- if (this._cork && !this._corked) {
431
- this._corked = true;
432
- this._raw_response.cork(() => this._initiate_response());
433
- }
434
- else {
441
+ const raw = this._raw_response;
442
+ let sent = false;
443
+ let responded = false;
444
+ let write_offset = 0;
445
+ raw.cork(() => {
435
446
  this._initiate_response();
436
- }
437
- if (this.completed)
447
+ write_offset = raw.getWriteOffset();
448
+ [sent, responded] = this._uws_write_chunk(chunk, total_size);
449
+ if (sent || responded)
450
+ return;
451
+ this.drain((offset) => {
452
+ if (this.completed || !total_size) {
453
+ resolve();
454
+ return true;
455
+ }
456
+ const remaining = chunk.slice(offset - write_offset);
457
+ let flushed = false;
458
+ let has_responded = false;
459
+ raw.cork(() => {
460
+ ;
461
+ [flushed, has_responded] = this._uws_write_chunk(remaining, total_size);
462
+ });
463
+ if (has_responded) {
464
+ this._mark_completed();
465
+ resolve();
466
+ return true;
467
+ }
468
+ if (flushed)
469
+ resolve();
470
+ return flushed;
471
+ });
472
+ });
473
+ if (responded) {
474
+ this._mark_completed();
438
475
  return resolve();
439
- const write_offset = this._raw_response.getWriteOffset();
440
- const [sent] = this._uws_write_chunk(chunk, total_size);
476
+ }
441
477
  if (sent)
442
- return resolve();
443
- this.drain((offset) => {
444
- if (this.completed || !total_size) {
445
- resolve();
446
- return true;
447
- }
448
- const remaining = chunk.slice(offset - write_offset);
449
- const [flushed] = this._uws_write_chunk(remaining, total_size);
450
- if (flushed)
451
- resolve();
452
- return flushed;
453
- });
478
+ resolve();
454
479
  });
455
480
  }
456
481
  /**
@@ -483,7 +508,7 @@ export default class Response {
483
508
  }
484
509
  if (!this.completed) {
485
510
  if (total_size)
486
- this.engine?._resolve_pending_request();
511
+ this._engine?._resolve_pending_request();
487
512
  else
488
513
  this.send();
489
514
  }
@@ -495,7 +520,7 @@ export default class Response {
495
520
  if (this.completed)
496
521
  return;
497
522
  this.completed = true;
498
- this.engine?._resolve_pending_request();
523
+ this._engine?._resolve_pending_request();
499
524
  //this._wrapped_request.pause()
500
525
  this._raw_response.close();
501
526
  }
@@ -509,24 +534,18 @@ export default class Response {
509
534
  }
510
535
  /**
511
536
  * Fast-path send used by the `_invokeHandler`.
512
- * Skips cork/uncork logic and writes directly.
537
+ * Corks the writes so status, headers and body are flushed atomically.
513
538
  */
514
539
  _sendFast(body) {
540
+ if (this.completed)
541
+ return;
542
+ const raw = this._raw_response;
543
+ raw.cork(() => {
544
+ this._initiate_response();
545
+ raw.end(body);
546
+ });
515
547
  this._corked = true;
516
- this._initiate_response();
517
- this._raw_response.end(body);
518
- this.completed = true;
519
- this.engine?._resolve_pending_request();
520
- if (this._events?.finish) {
521
- for (let i = 0; i < this._events.finish.length; i++) {
522
- this._events.finish[i](this._wrapped_request, this);
523
- }
524
- }
525
- if (this._events?.close) {
526
- for (let i = 0; i < this._events.close.length; i++) {
527
- this._events.close[i](this._wrapped_request, this);
528
- }
529
- }
548
+ this._mark_completed();
530
549
  }
531
550
  /**
532
551
  * Sends a JSON response. Sets `Content-Type: application/json` and
@@ -534,6 +553,7 @@ export default class Response {
534
553
  */
535
554
  json(body) {
536
555
  this._headers["content-type"] = "application/json";
556
+ this._has_headers = true;
537
557
  return this.send(stringify(body));
538
558
  }
539
559
  /**
@@ -541,6 +561,7 @@ export default class Response {
541
561
  */
542
562
  html(body) {
543
563
  this._headers["content-type"] = "text/html";
564
+ this._has_headers = true;
544
565
  return this.send(body);
545
566
  }
546
567
  /**
@@ -711,6 +732,7 @@ export default class Response {
711
732
  */
712
733
  location(path) {
713
734
  this._headers["location"] = path;
735
+ this._has_headers = true;
714
736
  return this;
715
737
  }
716
738
  /**
@@ -737,6 +759,7 @@ export default class Response {
737
759
  */
738
760
  vary(name) {
739
761
  this._headers["vary"] = name;
762
+ this._has_headers = true;
740
763
  return this;
741
764
  }
742
765
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linebridge/engine-neo",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },