@bxb1337/windsurf-fast-context 1.0.8 → 1.0.9

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.
@@ -56,7 +56,7 @@ class DevstralLanguageModel {
56
56
  const requestFrame = (0, connect_frame_js_1.connectFrameEncode)(requestPayload);
57
57
  const headers = createConnectHeaders(this.headers);
58
58
  const responseFrame = await this.transport.postUnary(`${this.baseURL}${API_SERVICE_PATH}${DEVSTRAL_STREAM_PATH}`, requestFrame, headers);
59
- const responsePayloads = (0, connect_frame_js_1.connectFrameDecode)(responseFrame);
59
+ const { payloads: responsePayloads } = (0, connect_frame_js_1.connectFrameDecode)(responseFrame);
60
60
  const payloads = responsePayloads.length > 0 ? responsePayloads : [responseFrame];
61
61
  const content = payloads.flatMap((payload) => toV3Content((0, response_converter_js_1.convertResponse)(payload)));
62
62
  const unified = content.some((part) => part.type === 'tool-call')
@@ -110,7 +110,7 @@ class DevstralLanguageModel {
110
110
  try {
111
111
  safeEnqueue(controller, { type: 'stream-start', warnings: [] });
112
112
  safeEnqueue(controller, { type: 'response-metadata', modelId: this.modelId });
113
- while (!isAborted(options.abortSignal)) {
113
+ outerLoop: while (!isAborted(options.abortSignal)) {
114
114
  const next = await reader.read();
115
115
  if (next.done) {
116
116
  break;
@@ -162,6 +162,9 @@ class DevstralLanguageModel {
162
162
  });
163
163
  safeEnqueue(controller, part);
164
164
  }
165
+ if (frameResult.isEndStream) {
166
+ break outerLoop;
167
+ }
165
168
  }
166
169
  }
167
170
  if (isAborted(options.abortSignal)) {
@@ -219,10 +222,11 @@ function readNextConnectFrame(buffer) {
219
222
  return null;
220
223
  }
221
224
  const frame = buffer.subarray(0, frameLength);
222
- const decoded = (0, connect_frame_js_1.connectFrameDecode)(frame);
225
+ const { payloads, isEndStream } = (0, connect_frame_js_1.connectFrameDecode)(frame);
223
226
  return {
224
- payload: decoded[0] ?? Buffer.alloc(0),
227
+ payload: payloads[0] ?? Buffer.alloc(0),
225
228
  rest: buffer.subarray(frameLength),
229
+ isEndStream,
226
230
  };
227
231
  }
228
232
  function safeEnqueue(controller, part) {
@@ -66,8 +66,8 @@ function waitFor(ms) {
66
66
  }
67
67
  function decodeRequestPayload(body) {
68
68
  // Body is now a connect frame directly (gzip is inside the frame, not outside)
69
- const decodedFrames = (0, connect_frame_js_1.connectFrameDecode)(body);
70
- return decodedFrames[0] ?? Buffer.alloc(0);
69
+ const { payloads } = (0, connect_frame_js_1.connectFrameDecode)(body);
70
+ return payloads[0] ?? Buffer.alloc(0);
71
71
  }
72
72
  function extractToolsPayload(strings) {
73
73
  return strings.find((value) => value.startsWith('[{"type":"function"'));
@@ -467,6 +467,76 @@ async function collectStreamParts(stream) {
467
467
  },
468
468
  });
469
469
  });
470
+ (0, vitest_1.it)('stream terminates immediately on EndStreamResponse frame (flags=2)', async () => {
471
+ const jwt = makeJwt(4_300_000_003, 'end-stream-response');
472
+ const textFrame = (0, connect_frame_js_1.connectFrameEncode)(Buffer.from('hello world', 'utf8'));
473
+ const endStreamPayload = Buffer.alloc(0);
474
+ const endStreamFrame = Buffer.allocUnsafe(5 + endStreamPayload.length);
475
+ endStreamFrame.writeUInt8(2, 0);
476
+ endStreamFrame.writeUInt32BE(endStreamPayload.length, 1);
477
+ endStreamPayload.copy(endStreamFrame, 5);
478
+ let releaseHttpClose;
479
+ const waitForHttpClose = new Promise((resolve) => {
480
+ releaseHttpClose = resolve;
481
+ });
482
+ const fakeFetch = async (input) => {
483
+ const url = String(input);
484
+ if (url.endsWith('/GetUserJwt')) {
485
+ return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
486
+ }
487
+ let sentTextFrame = false;
488
+ let sentEndStreamFrame = false;
489
+ return new Response(new ReadableStream({
490
+ async pull(controller) {
491
+ if (!sentTextFrame) {
492
+ controller.enqueue(Uint8Array.from(textFrame));
493
+ sentTextFrame = true;
494
+ return;
495
+ }
496
+ if (!sentEndStreamFrame) {
497
+ controller.enqueue(Uint8Array.from(endStreamFrame));
498
+ sentEndStreamFrame = true;
499
+ return;
500
+ }
501
+ await waitForHttpClose;
502
+ controller.close();
503
+ },
504
+ }), { status: 200 });
505
+ };
506
+ const model = new devstral_language_model_js_1.DevstralLanguageModel({
507
+ apiKey: 'end-stream-key',
508
+ fetch: fakeFetch,
509
+ baseURL: 'https://windsurf.test',
510
+ });
511
+ const result = await model.doStream({
512
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'Stream with end.' }] }],
513
+ });
514
+ const reader = result.stream.getReader();
515
+ const parts = [];
516
+ const resultType = await Promise.race([
517
+ (async () => {
518
+ while (true) {
519
+ const next = await reader.read();
520
+ if (next.done) {
521
+ break;
522
+ }
523
+ parts.push(next.value);
524
+ }
525
+ return 'completed';
526
+ })(),
527
+ waitFor(100).then(() => 'timed-out'),
528
+ ]);
529
+ (0, vitest_1.expect)(resultType).toBe('completed');
530
+ (0, vitest_1.expect)(parts.map((p) => p.type)).toEqual([
531
+ 'stream-start',
532
+ 'response-metadata',
533
+ 'text-start',
534
+ 'text-delta',
535
+ 'text-end',
536
+ 'finish',
537
+ ]);
538
+ releaseHttpClose?.();
539
+ });
470
540
  (0, vitest_1.it)('abort stops stream mid-response', async () => {
471
541
  const controller = new AbortController();
472
542
  const jwt = makeJwt(4_300_000_002, 'abort');
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FLAG_END_STREAM = exports.FLAG_COMPRESSED = void 0;
3
4
  exports.connectFrameEncode = connectFrameEncode;
4
5
  exports.connectFrameDecode = connectFrameDecode;
5
6
  const node_zlib_1 = require("node:zlib");
6
7
  const FRAME_HEADER_SIZE = 5;
7
- const FLAG_COMPRESSED = 1;
8
+ exports.FLAG_COMPRESSED = 1;
9
+ exports.FLAG_END_STREAM = 2;
8
10
  /**
9
11
  * Encode a protobuf payload into a Connect-RPC frame.
10
12
  *
@@ -16,7 +18,7 @@ const FLAG_COMPRESSED = 1;
16
18
  function connectFrameEncode(payload, compress = true) {
17
19
  const payloadBytes = compress ? (0, node_zlib_1.gzipSync)(payload) : payload;
18
20
  const frame = Buffer.allocUnsafe(FRAME_HEADER_SIZE + payloadBytes.length);
19
- frame.writeUInt8(compress ? FLAG_COMPRESSED : 0, 0);
21
+ frame.writeUInt8(compress ? exports.FLAG_COMPRESSED : 0, 0);
20
22
  frame.writeUInt32BE(payloadBytes.length, 1);
21
23
  payloadBytes.copy(frame, FRAME_HEADER_SIZE);
22
24
  return frame;
@@ -24,6 +26,7 @@ function connectFrameEncode(payload, compress = true) {
24
26
  function connectFrameDecode(buffer) {
25
27
  const decoded = [];
26
28
  let offset = 0;
29
+ let isEndStream = false;
27
30
  while (offset + FRAME_HEADER_SIZE <= buffer.length) {
28
31
  const flags = buffer.readUInt8(offset);
29
32
  const payloadLength = buffer.readUInt32BE(offset + 1);
@@ -33,8 +36,11 @@ function connectFrameDecode(buffer) {
33
36
  break;
34
37
  }
35
38
  const payload = buffer.subarray(payloadStart, payloadEnd);
36
- decoded.push(flags === FLAG_COMPRESSED ? (0, node_zlib_1.gunzipSync)(payload) : Buffer.from(payload));
39
+ decoded.push((flags & exports.FLAG_COMPRESSED) !== 0 ? (0, node_zlib_1.gunzipSync)(payload) : Buffer.from(payload));
40
+ if ((flags & exports.FLAG_END_STREAM) !== 0) {
41
+ isEndStream = true;
42
+ }
37
43
  offset = payloadEnd;
38
44
  }
39
- return decoded;
45
+ return { payloads: decoded, isEndStream };
40
46
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_zlib_1 = require("node:zlib");
3
4
  const node_fs_1 = require("node:fs");
4
5
  const node_path_1 = require("node:path");
5
6
  const vitest_1 = require("vitest");
@@ -15,22 +16,94 @@ const payloadWorld = Buffer.from('CONNECT\x02world', 'utf8');
15
16
  (0, vitest_1.expect)(encoded.readUInt8(0)).toBe(0);
16
17
  (0, vitest_1.expect)(encoded.readUInt32BE(1)).toBe(payloadHello.length);
17
18
  (0, vitest_1.expect)(encoded.subarray(5)).toEqual(payloadHello);
18
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(encoded)).toEqual([payloadHello]);
19
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(encoded)).toEqual({ payloads: [payloadHello], isEndStream: false });
19
20
  });
20
21
  (0, vitest_1.it)('gzip roundtrip uses compressed frame and decodes back to source payload', () => {
21
22
  const encoded = (0, connect_frame_js_1.connectFrameEncode)(payloadWorld, true);
22
23
  (0, vitest_1.expect)(encoded.readUInt8(0)).toBe(1);
23
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(encoded)).toEqual([payloadWorld]);
24
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(encoded)).toEqual({ payloads: [payloadWorld], isEndStream: false });
24
25
  });
25
26
  });
26
27
  (0, vitest_1.describe)('connectFrameDecode fixtures', () => {
27
28
  (0, vitest_1.it)('decodes frame-simple fixture payload', () => {
28
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureSimpleFrame)).toEqual([payloadHello]);
29
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureSimpleFrame)).toEqual({ payloads: [payloadHello], isEndStream: false });
29
30
  });
30
31
  (0, vitest_1.it)('decodes gzip fixture payload', () => {
31
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureGzipFrame)).toEqual([payloadWorld]);
32
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureGzipFrame)).toEqual({ payloads: [payloadWorld], isEndStream: false });
32
33
  });
33
34
  (0, vitest_1.it)('decodes multiple concatenated frames in order', () => {
34
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureMultiFrame)).toEqual([payloadHello, payloadHello]);
35
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureMultiFrame)).toEqual({ payloads: [payloadHello, payloadHello], isEndStream: false });
36
+ });
37
+ });
38
+ (0, vitest_1.describe)('connect frame flags', () => {
39
+ (0, vitest_1.it)('exports FLAG_COMPRESSED constant with value 1', () => {
40
+ (0, vitest_1.expect)(connect_frame_js_1.FLAG_COMPRESSED).toBe(1);
41
+ });
42
+ (0, vitest_1.it)('exports FLAG_END_STREAM constant with value 2', () => {
43
+ (0, vitest_1.expect)(connect_frame_js_1.FLAG_END_STREAM).toBe(2);
44
+ });
45
+ (0, vitest_1.it)('supports bit combination: FLAG_COMPRESSED | FLAG_END_STREAM === 3', () => {
46
+ (0, vitest_1.expect)(connect_frame_js_1.FLAG_COMPRESSED | connect_frame_js_1.FLAG_END_STREAM).toBe(3);
47
+ });
48
+ });
49
+ (0, vitest_1.describe)('connectFrameDecode isEndStream', () => {
50
+ /**
51
+ * TDD RED PHASE: Tests for isEndStream flag detection.
52
+ * These tests MUST FAIL because connectFrameDecode currently returns Buffer[],
53
+ * not { payloads: Buffer[], isEndStream: boolean }.
54
+ *
55
+ * Connect-RPC flag bits:
56
+ * - Bit 0 (value 1): FLAG_COMPRESSED
57
+ * - Bit 1 (value 2): FLAG_END_STREAM
58
+ */
59
+ (0, vitest_1.it)('returns isEndStream: false for flags=0 (no flags)', () => {
60
+ // flags=0: neither compressed nor end stream
61
+ const frame = Buffer.allocUnsafe(5 + payloadHello.length);
62
+ frame.writeUInt8(0, 0); // flags = 0
63
+ frame.writeUInt32BE(payloadHello.length, 1);
64
+ payloadHello.copy(frame, 5);
65
+ const result = (0, connect_frame_js_1.connectFrameDecode)(frame);
66
+ (0, vitest_1.expect)(result).toHaveProperty('payloads');
67
+ (0, vitest_1.expect)(result).toHaveProperty('isEndStream');
68
+ (0, vitest_1.expect)(result.payloads).toEqual([payloadHello]);
69
+ (0, vitest_1.expect)(result.isEndStream).toBe(false);
70
+ });
71
+ (0, vitest_1.it)('returns isEndStream: false for flags=1 (compressed only)', () => {
72
+ // flags=1: compressed but not end stream
73
+ const gzipped = (0, node_zlib_1.gzipSync)(payloadHello);
74
+ const frame = Buffer.allocUnsafe(5 + gzipped.length);
75
+ frame.writeUInt8(connect_frame_js_1.FLAG_COMPRESSED, 0); // flags = 1
76
+ frame.writeUInt32BE(gzipped.length, 1);
77
+ gzipped.copy(frame, 5);
78
+ const result = (0, connect_frame_js_1.connectFrameDecode)(frame);
79
+ (0, vitest_1.expect)(result).toHaveProperty('payloads');
80
+ (0, vitest_1.expect)(result).toHaveProperty('isEndStream');
81
+ (0, vitest_1.expect)(result.payloads).toEqual([payloadHello]);
82
+ (0, vitest_1.expect)(result.isEndStream).toBe(false);
83
+ });
84
+ (0, vitest_1.it)('returns isEndStream: true for flags=2 (end stream only)', () => {
85
+ // flags=2: end stream but not compressed
86
+ const frame = Buffer.allocUnsafe(5 + payloadHello.length);
87
+ frame.writeUInt8(connect_frame_js_1.FLAG_END_STREAM, 0); // flags = 2
88
+ frame.writeUInt32BE(payloadHello.length, 1);
89
+ payloadHello.copy(frame, 5);
90
+ const result = (0, connect_frame_js_1.connectFrameDecode)(frame);
91
+ (0, vitest_1.expect)(result).toHaveProperty('payloads');
92
+ (0, vitest_1.expect)(result).toHaveProperty('isEndStream');
93
+ (0, vitest_1.expect)(result.payloads).toEqual([payloadHello]);
94
+ (0, vitest_1.expect)(result.isEndStream).toBe(true);
95
+ });
96
+ (0, vitest_1.it)('returns isEndStream: true for flags=3 (compressed + end stream)', () => {
97
+ // flags=3: both compressed and end stream
98
+ const gzipped = (0, node_zlib_1.gzipSync)(payloadHello);
99
+ const frame = Buffer.allocUnsafe(5 + gzipped.length);
100
+ frame.writeUInt8(connect_frame_js_1.FLAG_COMPRESSED | connect_frame_js_1.FLAG_END_STREAM, 0); // flags = 3
101
+ frame.writeUInt32BE(gzipped.length, 1);
102
+ gzipped.copy(frame, 5);
103
+ const result = (0, connect_frame_js_1.connectFrameDecode)(frame);
104
+ (0, vitest_1.expect)(result).toHaveProperty('payloads');
105
+ (0, vitest_1.expect)(result).toHaveProperty('isEndStream');
106
+ (0, vitest_1.expect)(result.payloads).toEqual([payloadHello]);
107
+ (0, vitest_1.expect)(result.isEndStream).toBe(true);
35
108
  });
36
109
  });
@@ -53,7 +53,7 @@ export class DevstralLanguageModel {
53
53
  const requestFrame = connectFrameEncode(requestPayload);
54
54
  const headers = createConnectHeaders(this.headers);
55
55
  const responseFrame = await this.transport.postUnary(`${this.baseURL}${API_SERVICE_PATH}${DEVSTRAL_STREAM_PATH}`, requestFrame, headers);
56
- const responsePayloads = connectFrameDecode(responseFrame);
56
+ const { payloads: responsePayloads } = connectFrameDecode(responseFrame);
57
57
  const payloads = responsePayloads.length > 0 ? responsePayloads : [responseFrame];
58
58
  const content = payloads.flatMap((payload) => toV3Content(convertResponse(payload)));
59
59
  const unified = content.some((part) => part.type === 'tool-call')
@@ -107,7 +107,7 @@ export class DevstralLanguageModel {
107
107
  try {
108
108
  safeEnqueue(controller, { type: 'stream-start', warnings: [] });
109
109
  safeEnqueue(controller, { type: 'response-metadata', modelId: this.modelId });
110
- while (!isAborted(options.abortSignal)) {
110
+ outerLoop: while (!isAborted(options.abortSignal)) {
111
111
  const next = await reader.read();
112
112
  if (next.done) {
113
113
  break;
@@ -159,6 +159,9 @@ export class DevstralLanguageModel {
159
159
  });
160
160
  safeEnqueue(controller, part);
161
161
  }
162
+ if (frameResult.isEndStream) {
163
+ break outerLoop;
164
+ }
162
165
  }
163
166
  }
164
167
  if (isAborted(options.abortSignal)) {
@@ -215,10 +218,11 @@ function readNextConnectFrame(buffer) {
215
218
  return null;
216
219
  }
217
220
  const frame = buffer.subarray(0, frameLength);
218
- const decoded = connectFrameDecode(frame);
221
+ const { payloads, isEndStream } = connectFrameDecode(frame);
219
222
  return {
220
- payload: decoded[0] ?? Buffer.alloc(0),
223
+ payload: payloads[0] ?? Buffer.alloc(0),
221
224
  rest: buffer.subarray(frameLength),
225
+ isEndStream,
222
226
  };
223
227
  }
224
228
  function safeEnqueue(controller, part) {
@@ -64,8 +64,8 @@ function waitFor(ms) {
64
64
  }
65
65
  function decodeRequestPayload(body) {
66
66
  // Body is now a connect frame directly (gzip is inside the frame, not outside)
67
- const decodedFrames = connectFrameDecode(body);
68
- return decodedFrames[0] ?? Buffer.alloc(0);
67
+ const { payloads } = connectFrameDecode(body);
68
+ return payloads[0] ?? Buffer.alloc(0);
69
69
  }
70
70
  function extractToolsPayload(strings) {
71
71
  return strings.find((value) => value.startsWith('[{"type":"function"'));
@@ -465,6 +465,76 @@ describe('DevstralLanguageModel doStream', () => {
465
465
  },
466
466
  });
467
467
  });
468
+ it('stream terminates immediately on EndStreamResponse frame (flags=2)', async () => {
469
+ const jwt = makeJwt(4_300_000_003, 'end-stream-response');
470
+ const textFrame = connectFrameEncode(Buffer.from('hello world', 'utf8'));
471
+ const endStreamPayload = Buffer.alloc(0);
472
+ const endStreamFrame = Buffer.allocUnsafe(5 + endStreamPayload.length);
473
+ endStreamFrame.writeUInt8(2, 0);
474
+ endStreamFrame.writeUInt32BE(endStreamPayload.length, 1);
475
+ endStreamPayload.copy(endStreamFrame, 5);
476
+ let releaseHttpClose;
477
+ const waitForHttpClose = new Promise((resolve) => {
478
+ releaseHttpClose = resolve;
479
+ });
480
+ const fakeFetch = async (input) => {
481
+ const url = String(input);
482
+ if (url.endsWith('/GetUserJwt')) {
483
+ return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
484
+ }
485
+ let sentTextFrame = false;
486
+ let sentEndStreamFrame = false;
487
+ return new Response(new ReadableStream({
488
+ async pull(controller) {
489
+ if (!sentTextFrame) {
490
+ controller.enqueue(Uint8Array.from(textFrame));
491
+ sentTextFrame = true;
492
+ return;
493
+ }
494
+ if (!sentEndStreamFrame) {
495
+ controller.enqueue(Uint8Array.from(endStreamFrame));
496
+ sentEndStreamFrame = true;
497
+ return;
498
+ }
499
+ await waitForHttpClose;
500
+ controller.close();
501
+ },
502
+ }), { status: 200 });
503
+ };
504
+ const model = new DevstralLanguageModel({
505
+ apiKey: 'end-stream-key',
506
+ fetch: fakeFetch,
507
+ baseURL: 'https://windsurf.test',
508
+ });
509
+ const result = await model.doStream({
510
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'Stream with end.' }] }],
511
+ });
512
+ const reader = result.stream.getReader();
513
+ const parts = [];
514
+ const resultType = await Promise.race([
515
+ (async () => {
516
+ while (true) {
517
+ const next = await reader.read();
518
+ if (next.done) {
519
+ break;
520
+ }
521
+ parts.push(next.value);
522
+ }
523
+ return 'completed';
524
+ })(),
525
+ waitFor(100).then(() => 'timed-out'),
526
+ ]);
527
+ expect(resultType).toBe('completed');
528
+ expect(parts.map((p) => p.type)).toEqual([
529
+ 'stream-start',
530
+ 'response-metadata',
531
+ 'text-start',
532
+ 'text-delta',
533
+ 'text-end',
534
+ 'finish',
535
+ ]);
536
+ releaseHttpClose?.();
537
+ });
468
538
  it('abort stops stream mid-response', async () => {
469
539
  const controller = new AbortController();
470
540
  const jwt = makeJwt(4_300_000_002, 'abort');
@@ -1,6 +1,7 @@
1
1
  import { gunzipSync, gzipSync } from 'node:zlib';
2
2
  const FRAME_HEADER_SIZE = 5;
3
- const FLAG_COMPRESSED = 1;
3
+ export const FLAG_COMPRESSED = 1;
4
+ export const FLAG_END_STREAM = 2;
4
5
  /**
5
6
  * Encode a protobuf payload into a Connect-RPC frame.
6
7
  *
@@ -20,6 +21,7 @@ export function connectFrameEncode(payload, compress = true) {
20
21
  export function connectFrameDecode(buffer) {
21
22
  const decoded = [];
22
23
  let offset = 0;
24
+ let isEndStream = false;
23
25
  while (offset + FRAME_HEADER_SIZE <= buffer.length) {
24
26
  const flags = buffer.readUInt8(offset);
25
27
  const payloadLength = buffer.readUInt32BE(offset + 1);
@@ -29,8 +31,11 @@ export function connectFrameDecode(buffer) {
29
31
  break;
30
32
  }
31
33
  const payload = buffer.subarray(payloadStart, payloadEnd);
32
- decoded.push(flags === FLAG_COMPRESSED ? gunzipSync(payload) : Buffer.from(payload));
34
+ decoded.push((flags & FLAG_COMPRESSED) !== 0 ? gunzipSync(payload) : Buffer.from(payload));
35
+ if ((flags & FLAG_END_STREAM) !== 0) {
36
+ isEndStream = true;
37
+ }
33
38
  offset = payloadEnd;
34
39
  }
35
- return decoded;
40
+ return { payloads: decoded, isEndStream };
36
41
  }
@@ -1,7 +1,8 @@
1
+ import { gzipSync } from 'node:zlib';
1
2
  import { readFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { describe, expect, it } from 'vitest';
4
- import { connectFrameDecode, connectFrameEncode } from './connect-frame.js';
5
+ import { connectFrameDecode, connectFrameEncode, FLAG_COMPRESSED, FLAG_END_STREAM } from './connect-frame.js';
5
6
  const fixtureSimpleFrame = readFileSync(join(process.cwd(), 'test/fixtures/connect/frame-simple.bin'));
6
7
  const fixtureGzipFrame = readFileSync(join(process.cwd(), 'test/fixtures/connect/frame-gzip.bin'));
7
8
  const fixtureMultiFrame = readFileSync(join(process.cwd(), 'test/fixtures/connect/frame-multi.bin'));
@@ -13,22 +14,94 @@ describe('connectFrameEncode', () => {
13
14
  expect(encoded.readUInt8(0)).toBe(0);
14
15
  expect(encoded.readUInt32BE(1)).toBe(payloadHello.length);
15
16
  expect(encoded.subarray(5)).toEqual(payloadHello);
16
- expect(connectFrameDecode(encoded)).toEqual([payloadHello]);
17
+ expect(connectFrameDecode(encoded)).toEqual({ payloads: [payloadHello], isEndStream: false });
17
18
  });
18
19
  it('gzip roundtrip uses compressed frame and decodes back to source payload', () => {
19
20
  const encoded = connectFrameEncode(payloadWorld, true);
20
21
  expect(encoded.readUInt8(0)).toBe(1);
21
- expect(connectFrameDecode(encoded)).toEqual([payloadWorld]);
22
+ expect(connectFrameDecode(encoded)).toEqual({ payloads: [payloadWorld], isEndStream: false });
22
23
  });
23
24
  });
24
25
  describe('connectFrameDecode fixtures', () => {
25
26
  it('decodes frame-simple fixture payload', () => {
26
- expect(connectFrameDecode(fixtureSimpleFrame)).toEqual([payloadHello]);
27
+ expect(connectFrameDecode(fixtureSimpleFrame)).toEqual({ payloads: [payloadHello], isEndStream: false });
27
28
  });
28
29
  it('decodes gzip fixture payload', () => {
29
- expect(connectFrameDecode(fixtureGzipFrame)).toEqual([payloadWorld]);
30
+ expect(connectFrameDecode(fixtureGzipFrame)).toEqual({ payloads: [payloadWorld], isEndStream: false });
30
31
  });
31
32
  it('decodes multiple concatenated frames in order', () => {
32
- expect(connectFrameDecode(fixtureMultiFrame)).toEqual([payloadHello, payloadHello]);
33
+ expect(connectFrameDecode(fixtureMultiFrame)).toEqual({ payloads: [payloadHello, payloadHello], isEndStream: false });
34
+ });
35
+ });
36
+ describe('connect frame flags', () => {
37
+ it('exports FLAG_COMPRESSED constant with value 1', () => {
38
+ expect(FLAG_COMPRESSED).toBe(1);
39
+ });
40
+ it('exports FLAG_END_STREAM constant with value 2', () => {
41
+ expect(FLAG_END_STREAM).toBe(2);
42
+ });
43
+ it('supports bit combination: FLAG_COMPRESSED | FLAG_END_STREAM === 3', () => {
44
+ expect(FLAG_COMPRESSED | FLAG_END_STREAM).toBe(3);
45
+ });
46
+ });
47
+ describe('connectFrameDecode isEndStream', () => {
48
+ /**
49
+ * TDD RED PHASE: Tests for isEndStream flag detection.
50
+ * These tests MUST FAIL because connectFrameDecode currently returns Buffer[],
51
+ * not { payloads: Buffer[], isEndStream: boolean }.
52
+ *
53
+ * Connect-RPC flag bits:
54
+ * - Bit 0 (value 1): FLAG_COMPRESSED
55
+ * - Bit 1 (value 2): FLAG_END_STREAM
56
+ */
57
+ it('returns isEndStream: false for flags=0 (no flags)', () => {
58
+ // flags=0: neither compressed nor end stream
59
+ const frame = Buffer.allocUnsafe(5 + payloadHello.length);
60
+ frame.writeUInt8(0, 0); // flags = 0
61
+ frame.writeUInt32BE(payloadHello.length, 1);
62
+ payloadHello.copy(frame, 5);
63
+ const result = connectFrameDecode(frame);
64
+ expect(result).toHaveProperty('payloads');
65
+ expect(result).toHaveProperty('isEndStream');
66
+ expect(result.payloads).toEqual([payloadHello]);
67
+ expect(result.isEndStream).toBe(false);
68
+ });
69
+ it('returns isEndStream: false for flags=1 (compressed only)', () => {
70
+ // flags=1: compressed but not end stream
71
+ const gzipped = gzipSync(payloadHello);
72
+ const frame = Buffer.allocUnsafe(5 + gzipped.length);
73
+ frame.writeUInt8(FLAG_COMPRESSED, 0); // flags = 1
74
+ frame.writeUInt32BE(gzipped.length, 1);
75
+ gzipped.copy(frame, 5);
76
+ const result = connectFrameDecode(frame);
77
+ expect(result).toHaveProperty('payloads');
78
+ expect(result).toHaveProperty('isEndStream');
79
+ expect(result.payloads).toEqual([payloadHello]);
80
+ expect(result.isEndStream).toBe(false);
81
+ });
82
+ it('returns isEndStream: true for flags=2 (end stream only)', () => {
83
+ // flags=2: end stream but not compressed
84
+ const frame = Buffer.allocUnsafe(5 + payloadHello.length);
85
+ frame.writeUInt8(FLAG_END_STREAM, 0); // flags = 2
86
+ frame.writeUInt32BE(payloadHello.length, 1);
87
+ payloadHello.copy(frame, 5);
88
+ const result = connectFrameDecode(frame);
89
+ expect(result).toHaveProperty('payloads');
90
+ expect(result).toHaveProperty('isEndStream');
91
+ expect(result.payloads).toEqual([payloadHello]);
92
+ expect(result.isEndStream).toBe(true);
93
+ });
94
+ it('returns isEndStream: true for flags=3 (compressed + end stream)', () => {
95
+ // flags=3: both compressed and end stream
96
+ const gzipped = gzipSync(payloadHello);
97
+ const frame = Buffer.allocUnsafe(5 + gzipped.length);
98
+ frame.writeUInt8(FLAG_COMPRESSED | FLAG_END_STREAM, 0); // flags = 3
99
+ frame.writeUInt32BE(gzipped.length, 1);
100
+ gzipped.copy(frame, 5);
101
+ const result = connectFrameDecode(frame);
102
+ expect(result).toHaveProperty('payloads');
103
+ expect(result).toHaveProperty('isEndStream');
104
+ expect(result.payloads).toEqual([payloadHello]);
105
+ expect(result.isEndStream).toBe(true);
33
106
  });
34
107
  });
@@ -1,3 +1,5 @@
1
+ export declare const FLAG_COMPRESSED = 1;
2
+ export declare const FLAG_END_STREAM = 2;
1
3
  /**
2
4
  * Encode a protobuf payload into a Connect-RPC frame.
3
5
  *
@@ -7,4 +9,7 @@
7
9
  * @returns Buffer containing: [flags byte][4-byte length][payload (gzipped if compress=true)]
8
10
  */
9
11
  export declare function connectFrameEncode(payload: Buffer, compress?: boolean): Buffer;
10
- export declare function connectFrameDecode(buffer: Buffer): Buffer[];
12
+ export declare function connectFrameDecode(buffer: Buffer): {
13
+ payloads: Buffer[];
14
+ isEndStream: boolean;
15
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bxb1337/windsurf-fast-context",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "AI SDK V3 provider for Windsurf's Devstral code search API",
5
5
  "type": "module",
6
6
  "scripts": {