@s2-dev/streamstore 0.15.2 → 0.15.4

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.
Files changed (69) hide show
  1. package/README.md +4 -4
  2. package/bin/mcp-server.js +435 -585
  3. package/bin/mcp-server.js.map +13 -13
  4. package/dist/commonjs/funcs/recordsRead.js +1 -3
  5. package/dist/commonjs/funcs/recordsRead.js.map +1 -1
  6. package/dist/commonjs/lib/config.d.ts +3 -3
  7. package/dist/commonjs/lib/config.js +3 -3
  8. package/dist/commonjs/lib/event-streams.d.ts +4 -10
  9. package/dist/commonjs/lib/event-streams.d.ts.map +1 -1
  10. package/dist/commonjs/lib/event-streams.js +110 -194
  11. package/dist/commonjs/lib/event-streams.js.map +1 -1
  12. package/dist/commonjs/lib/matchers.d.ts.map +1 -1
  13. package/dist/commonjs/lib/matchers.js +1 -4
  14. package/dist/commonjs/lib/matchers.js.map +1 -1
  15. package/dist/commonjs/mcp-server/cli/start/command.d.ts.map +1 -1
  16. package/dist/commonjs/mcp-server/cli/start/command.js +7 -11
  17. package/dist/commonjs/mcp-server/cli/start/command.js.map +1 -1
  18. package/dist/commonjs/mcp-server/mcp-server.js +1 -1
  19. package/dist/commonjs/mcp-server/server.js +1 -1
  20. package/dist/commonjs/models/errors/apierror.d.ts.map +1 -1
  21. package/dist/commonjs/models/errors/apierror.js +8 -2
  22. package/dist/commonjs/models/errors/apierror.js.map +1 -1
  23. package/dist/commonjs/models/errors/errorresponse.d.ts.map +1 -1
  24. package/dist/commonjs/models/errors/errorresponse.js +1 -3
  25. package/dist/commonjs/models/errors/errorresponse.js.map +1 -1
  26. package/dist/commonjs/models/operations/read.d.ts.map +1 -1
  27. package/dist/commonjs/models/operations/read.js +4 -6
  28. package/dist/commonjs/models/operations/read.js.map +1 -1
  29. package/dist/esm/funcs/recordsRead.js +1 -3
  30. package/dist/esm/funcs/recordsRead.js.map +1 -1
  31. package/dist/esm/lib/config.d.ts +3 -3
  32. package/dist/esm/lib/config.js +3 -3
  33. package/dist/esm/lib/event-streams.d.ts +4 -10
  34. package/dist/esm/lib/event-streams.d.ts.map +1 -1
  35. package/dist/esm/lib/event-streams.js +110 -193
  36. package/dist/esm/lib/event-streams.js.map +1 -1
  37. package/dist/esm/lib/matchers.d.ts.map +1 -1
  38. package/dist/esm/lib/matchers.js +1 -4
  39. package/dist/esm/lib/matchers.js.map +1 -1
  40. package/dist/esm/mcp-server/cli/start/command.d.ts.map +1 -1
  41. package/dist/esm/mcp-server/cli/start/command.js +7 -11
  42. package/dist/esm/mcp-server/cli/start/command.js.map +1 -1
  43. package/dist/esm/mcp-server/mcp-server.js +1 -1
  44. package/dist/esm/mcp-server/server.js +1 -1
  45. package/dist/esm/models/errors/apierror.d.ts.map +1 -1
  46. package/dist/esm/models/errors/apierror.js +8 -2
  47. package/dist/esm/models/errors/apierror.js.map +1 -1
  48. package/dist/esm/models/errors/errorresponse.d.ts.map +1 -1
  49. package/dist/esm/models/errors/errorresponse.js +1 -3
  50. package/dist/esm/models/errors/errorresponse.js.map +1 -1
  51. package/dist/esm/models/operations/read.d.ts.map +1 -1
  52. package/dist/esm/models/operations/read.js +4 -6
  53. package/dist/esm/models/operations/read.js.map +1 -1
  54. package/examples/README.md +26 -0
  55. package/examples/accessTokensListAccessTokens.example.ts +24 -0
  56. package/examples/package-lock.json +615 -0
  57. package/examples/package.json +18 -0
  58. package/jsr.json +1 -1
  59. package/package.json +3 -3
  60. package/src/funcs/recordsRead.ts +1 -3
  61. package/src/lib/config.ts +3 -3
  62. package/src/lib/event-streams.ts +114 -231
  63. package/src/lib/matchers.ts +1 -4
  64. package/src/mcp-server/cli/start/command.ts +7 -12
  65. package/src/mcp-server/mcp-server.ts +1 -1
  66. package/src/mcp-server/server.ts +1 -1
  67. package/src/models/errors/apierror.ts +8 -2
  68. package/src/models/errors/errorresponse.ts +1 -3
  69. package/src/models/operations/read.ts +3 -6
@@ -5,260 +5,143 @@
5
5
  export type ServerEvent<T> = {
6
6
  data?: T | undefined;
7
7
  event?: string | undefined;
8
- retry?: number | undefined;
9
8
  id?: string | undefined;
9
+ retry?: number | undefined;
10
10
  };
11
- const LF = 0x0a;
12
- const CR = 0x0d;
13
- const NEWLINE_CHARS = new Set([LF, CR]);
14
- const MESSAGE_BOUNDARIES = [
15
- new Uint8Array([CR, LF, CR, LF]),
16
- new Uint8Array([CR, CR]),
17
- new Uint8Array([LF, LF]),
18
- ];
19
-
20
- export class EventStream<Event extends ServerEvent<unknown>> {
21
- private readonly stream: ReadableStream<Uint8Array>;
22
- private readonly decoder: (rawEvent: ServerEvent<string>) => Event;
23
-
24
- constructor(init: {
25
- stream: ReadableStream<Uint8Array>;
26
- decoder: (rawEvent: ServerEvent<string>) => Event;
27
- }) {
28
- this.stream = init.stream;
29
- this.decoder = init.decoder;
30
- }
31
-
32
- async *[Symbol.asyncIterator](): AsyncGenerator<Event, void, unknown> {
33
- const reader = this.stream.getReader();
34
- let buffer = new Uint8Array([]);
35
- let position = 0;
36
-
37
- try {
38
- while (true) {
39
- const { done, value } = await reader.read();
40
- if (done) {
41
- break;
42
- }
43
-
44
- const newBuffer = new Uint8Array(buffer.length + value.length);
45
- newBuffer.set(buffer);
46
- newBuffer.set(value, buffer.length);
47
- buffer = newBuffer;
48
-
49
- for (let i = position; i < buffer.length; i++) {
50
- const boundary = findBoundary(buffer, i);
51
- if (boundary == null) {
52
- continue;
53
- }
54
11
 
55
- const chunk = buffer.slice(position, i);
56
- position = i + boundary.length;
57
- const event = parseEvent(chunk, this.decoder);
58
- if (event != null) {
59
- yield event;
12
+ export class EventStream<T extends ServerEvent<unknown>>
13
+ extends ReadableStream<T>
14
+ {
15
+ constructor(
16
+ stream: ReadableStream<Uint8Array>,
17
+ parse: (x: ServerEvent<string>) => IteratorResult<T, undefined>,
18
+ ) {
19
+ const reader = stream.getReader();
20
+ let buffer: Uint8Array = new Uint8Array();
21
+ super({
22
+ async pull(controller) {
23
+ try {
24
+ const r = await reader.read();
25
+ if (r.done) return controller.close();
26
+ buffer = concatBuffer(buffer, r.value);
27
+ for (const { chunk, remainder } of chunks(buffer)) {
28
+ buffer = remainder;
29
+ const item = parseChunk(chunk, parse);
30
+ if (item?.value) controller.enqueue(item.value);
31
+ if (item?.done) {
32
+ await reader.cancel("done");
33
+ return controller.close();
34
+ }
60
35
  }
36
+ } catch (e) {
37
+ await reader.cancel(e);
38
+ controller.error(e);
61
39
  }
40
+ },
41
+ cancel: reason => reader.cancel(reason),
42
+ });
43
+ }
62
44
 
63
- if (position > 0) {
64
- buffer = buffer.slice(position);
65
- position = 0;
66
- }
67
- }
68
-
69
- if (buffer.length > 0) {
70
- const event = parseEvent(buffer, this.decoder);
71
- if (event != null) {
72
- yield event;
45
+ // Polyfill for older browsers
46
+ [Symbol.asyncIterator](): AsyncIterableIterator<T, void, unknown> {
47
+ const fn = (ReadableStream.prototype as any)[Symbol.asyncIterator];
48
+ if (typeof fn === "function") return fn.call(this);
49
+ const reader = this.getReader();
50
+ return {
51
+ next: async () => {
52
+ const r = await reader.read();
53
+ if (r.done) {
54
+ reader.releaseLock();
55
+ return { done: true, value: undefined };
73
56
  }
74
- }
75
- } catch (e: unknown) {
76
- if (e instanceof Error && e.name === "AbortError") {
77
- return;
78
- }
79
-
80
- throw e;
81
- } finally {
82
- reader.releaseLock();
83
- }
57
+ return { done: false, value: r.value };
58
+ },
59
+ throw: async (e) => {
60
+ await reader.cancel(e);
61
+ reader.releaseLock();
62
+ return { done: true, value: undefined };
63
+ },
64
+ return: async () => {
65
+ await reader.cancel("done");
66
+ reader.releaseLock();
67
+ return { done: true, value: undefined };
68
+ },
69
+ [Symbol.asyncIterator]() {
70
+ return this;
71
+ },
72
+ };
84
73
  }
85
74
  }
86
75
 
87
- function findBoundary(buffer: Uint8Array, start: number): Uint8Array | null {
88
- const char1 = buffer[start];
89
- const char2 = buffer[start + 1];
90
-
91
- // Don't bother checking if the first two characters are not new line
92
- // characters.
93
- if (
94
- char1 == null
95
- || char2 == null
96
- || !NEWLINE_CHARS.has(char1)
97
- || !NEWLINE_CHARS.has(char2)
98
- ) {
99
- return null;
100
- }
76
+ function concatBuffer(a: Uint8Array, b: Uint8Array): Uint8Array {
77
+ const c = new Uint8Array(a.length + b.length);
78
+ c.set(a, 0);
79
+ c.set(b, a.length);
80
+ return c;
81
+ }
101
82
 
102
- for (const s of MESSAGE_BOUNDARIES) {
103
- const seq = peekSequence(start, buffer, s);
104
- if (seq != null) {
105
- return seq;
83
+ /** Finds the first (CR,LF,CR,LF) or (CR,CR) or (LF,LF) */
84
+ function findBoundary(
85
+ buf: Uint8Array,
86
+ ): { index: number; length: number } | null {
87
+ const len = buf.length;
88
+ for (let i = 0; i < len; i++) {
89
+ if (
90
+ i <= len - 4
91
+ && buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13
92
+ && buf[i + 3] === 10
93
+ ) {
94
+ return { index: i, length: 4 };
95
+ }
96
+ if (i <= len - 2 && buf[i] === 13 && buf[i + 1] === 13) {
97
+ return { index: i, length: 2 };
98
+ }
99
+ if (i <= len - 2 && buf[i] === 10 && buf[i + 1] === 10) {
100
+ return { index: i, length: 2 };
106
101
  }
107
102
  }
108
-
109
103
  return null;
110
104
  }
111
105
 
112
- function peekSequence(
113
- position: number,
114
- buffer: Uint8Array,
115
- sequence: Uint8Array,
116
- ): Uint8Array | null {
117
- if (sequence.length > buffer.length - position) {
118
- return null;
119
- }
120
-
121
- for (let i = 0; i < sequence.length; i++) {
122
- if (buffer[position + i] !== sequence[i]) {
123
- return null;
106
+ function* chunks(
107
+ remainder: Uint8Array,
108
+ ): Generator<{ chunk: Uint8Array; remainder: Uint8Array }> {
109
+ while (true) {
110
+ const match = findBoundary(remainder);
111
+ if (!match) {
112
+ yield { chunk: new Uint8Array(), remainder };
113
+ return;
124
114
  }
115
+ const chunk = remainder.slice(0, match.index);
116
+ remainder = remainder.slice(match.index + match.length);
117
+ yield { chunk, remainder };
125
118
  }
126
-
127
- return sequence;
128
119
  }
129
120
 
130
- function parseEvent<Event extends ServerEvent<unknown>>(
121
+ function parseChunk<T extends ServerEvent<unknown>>(
131
122
  chunk: Uint8Array,
132
- decoder: (rawEvent: ServerEvent<string>) => Event,
123
+ parse: (x: ServerEvent<string>) => IteratorResult<T, undefined>,
133
124
  ) {
134
- if (!chunk.length) {
135
- return null;
136
- }
137
-
138
- const td = new TextDecoder();
139
- const raw = td.decode(chunk);
140
- const lines = raw.split(/\r?\n|\r/g);
141
- let publish = false;
142
- const rawEvent: ServerEvent<string> = {};
143
-
125
+ const text = new TextDecoder().decode(chunk);
126
+ const lines = text.split(/\r\n|\r|\n/);
127
+ const dataLines: string[] = [];
128
+ const ret: ServerEvent<string> = {};
129
+ let ignore = true;
144
130
  for (const line of lines) {
145
- if (!line) {
146
- continue;
147
- }
148
-
149
- const delim = line.indexOf(":");
150
- // Lines starting with a colon are ignored.
151
- if (delim === 0) {
152
- continue;
153
- }
154
-
155
- const field = delim > 0 ? line.substring(0, delim) : "";
156
- let value = delim > 0 ? line.substring(delim + 1) : "";
157
- if (value.charAt(0) === " ") {
158
- value = value.substring(1);
131
+ if (!line || line.startsWith(":")) continue;
132
+ ignore = false;
133
+ const i = line.indexOf(":");
134
+ const field = line.slice(0, i);
135
+ const value = line[i + 1] === " " ? line.slice(i + 2) : line.slice(i + 1);
136
+ if (field === "data") dataLines.push(value);
137
+ else if (field === "event") ret.event = value;
138
+ else if (field === "id") ret.id = value;
139
+ else if (field === "retry") {
140
+ const n = Number(value);
141
+ if (!isNaN(n)) ret.retry = n;
159
142
  }
160
-
161
- switch (field) {
162
- case "event": {
163
- publish = true;
164
- rawEvent.event = value;
165
- break;
166
- }
167
- case "data": {
168
- publish = true;
169
- rawEvent.data ??= "";
170
- rawEvent.data += value + "\n";
171
- break;
172
- }
173
- case "id": {
174
- publish = true;
175
- rawEvent.id = value;
176
- break;
177
- }
178
- case "retry": {
179
- const r = parseInt(value, 10);
180
- if (!Number.isNaN(r)) {
181
- publish = true;
182
- rawEvent.retry = r;
183
- }
184
- break;
185
- }
186
- }
187
- }
188
-
189
- if (!publish) {
190
- return null;
191
- }
192
-
193
- if (rawEvent.data != null) {
194
- rawEvent.data = rawEvent.data.slice(0, -1);
195
143
  }
196
-
197
- return decoder(rawEvent);
198
- }
199
-
200
- export function discardSentinel(
201
- stream: ReadableStream<Uint8Array>,
202
- sentinel: string,
203
- ): ReadableStream<Uint8Array> {
204
- return new ReadableStream<Uint8Array>({
205
- async start(controller) {
206
- let buffer = new Uint8Array([]);
207
- let position = 0;
208
- let done = false;
209
- let discard = false;
210
- const rdr = stream.getReader();
211
- try {
212
- while (!done) {
213
- const result = await rdr.read();
214
- const value = result.value;
215
- done = done || result.done;
216
- // We keep consuming from the source to its completion so it can
217
- // flush all its contents and release resources.
218
- if (discard) {
219
- continue;
220
- }
221
- if (typeof value === "undefined") {
222
- continue;
223
- }
224
-
225
- const newBuffer = new Uint8Array(buffer.length + value.length);
226
- newBuffer.set(buffer);
227
- newBuffer.set(value, buffer.length);
228
- buffer = newBuffer;
229
-
230
- for (let i = position; i < buffer.length; i++) {
231
- const boundary = findBoundary(buffer, i);
232
- if (boundary == null) {
233
- continue;
234
- }
235
-
236
- const start = position;
237
- const chunk = buffer.slice(start, i);
238
- position = i + boundary.length;
239
- const event = parseEvent(chunk, id);
240
- if (event?.data === sentinel) {
241
- controller.enqueue(buffer.slice(0, start));
242
- discard = true;
243
- } else {
244
- controller.enqueue(buffer.slice(0, position));
245
- buffer = buffer.slice(position);
246
- position = 0;
247
- }
248
- }
249
- }
250
- } catch (e) {
251
- controller.error(e);
252
- } finally {
253
- // If the source stream terminates, flush its contents and terminate.
254
- // If the sentinel event was found, flush everything up to its start.
255
- controller.close();
256
- rdr.releaseLock();
257
- }
258
- },
259
- });
260
- }
261
-
262
- function id<T>(v: T): T {
263
- return v;
144
+ if (ignore) return;
145
+ if (dataLines.length) ret.data = dataLines.join("\n");
146
+ return parse(ret);
264
147
  }
@@ -5,7 +5,6 @@
5
5
  import { APIError } from "../models/errors/apierror.js";
6
6
  import { ResponseValidationError } from "../models/errors/responsevalidationerror.js";
7
7
  import { ERR, OK, Result } from "../types/fp.js";
8
- import { discardSentinel } from "./event-streams.js";
9
8
  import { matchResponse, matchStatusCode, StatusCodePredicate } from "./http.js";
10
9
  import { isPlainObject } from "./is-plain-object.js";
11
10
 
@@ -238,9 +237,7 @@ export function match<T, E>(
238
237
  raw = body;
239
238
  break;
240
239
  case "sse":
241
- raw = response.body && matcher.sseSentinel
242
- ? discardSentinel(response.body, matcher.sseSentinel)
243
- : response.body;
240
+ raw = response.body;
244
241
  break;
245
242
  case "nil":
246
243
  body = await response.text();
@@ -37,18 +37,13 @@ export const startCommand = buildCommand({
37
37
  return z.string().parse(value);
38
38
  },
39
39
  },
40
- ...(mcpScopes.length
41
- ? {
42
- scope: {
43
- kind: "enum",
44
- brief:
45
- "Mount tools/resources that match given scope (repeatable flag)",
46
- values: mcpScopes,
47
- variadic: true,
48
- optional: true,
49
- },
50
- }
51
- : {}),
40
+ scope: {
41
+ kind: "enum",
42
+ brief: "Mount tools/resources that match given scope (repeatable flag)",
43
+ values: mcpScopes,
44
+ variadic: true,
45
+ optional: true,
46
+ },
52
47
  "access-token": {
53
48
  kind: "parsed",
54
49
  brief: "Sets the accessToken auth field for the API",
@@ -19,7 +19,7 @@ const routes = buildRouteMap({
19
19
  export const app = buildApplication(routes, {
20
20
  name: "mcp",
21
21
  versionInfo: {
22
- currentVersion: "0.15.2",
22
+ currentVersion: "0.15.4",
23
23
  },
24
24
  });
25
25
 
@@ -45,7 +45,7 @@ export function createMCPServer(deps: {
45
45
  }) {
46
46
  const server = new McpServer({
47
47
  name: "S2",
48
- version: "0.15.2",
48
+ version: "0.15.4",
49
49
  });
50
50
 
51
51
  const client = new S2Core({
@@ -25,8 +25,14 @@ export class APIError extends S2Error {
25
25
  }`;
26
26
  }
27
27
  const body = httpMeta.body || `""`;
28
- message += body.length > 100 ? "\n" : " ";
29
- message += `Body ${body}`;
28
+ message += body.length > 100 ? "\n" : ". ";
29
+ let bodyDisplay = body;
30
+ if (body.length > 10000) {
31
+ const truncated = body.substring(0, 10000);
32
+ const remaining = body.length - 10000;
33
+ bodyDisplay = `${truncated}...and ${remaining} more chars`;
34
+ }
35
+ message += `Body: ${bodyDisplay}`;
30
36
  message = message.trim();
31
37
  super(message, httpMeta);
32
38
  this.name = "APIError";
@@ -20,9 +20,7 @@ export class ErrorResponse extends S2Error {
20
20
  err: ErrorResponseData,
21
21
  httpMeta: { response: Response; request: Request; body: string },
22
22
  ) {
23
- const message = "message" in err && typeof err.message === "string"
24
- ? err.message
25
- : `API error occurred: ${JSON.stringify(err)}`;
23
+ const message = err.message || `API error occurred: ${JSON.stringify(err)}`;
26
24
  super(message, httpMeta);
27
25
  this.data$ = err;
28
26
  if (err.code != null) this.code = err.code;
@@ -169,12 +169,9 @@ export const ReadResponse$inboundSchema: z.ZodType<
169
169
  > = z.union([
170
170
  components.ReadBatch$inboundSchema,
171
171
  z.instanceof(ReadableStream<Uint8Array>).transform(stream => {
172
- return new EventStream({
173
- stream,
174
- decoder(rawEvent) {
175
- const schema = components.ReadEvent$inboundSchema;
176
- return schema.parse(rawEvent);
177
- },
172
+ return new EventStream(stream, rawEvent => {
173
+ if (rawEvent.data === "[DONE]") return { done: true };
174
+ return { value: components.ReadEvent$inboundSchema.parse(rawEvent) };
178
175
  });
179
176
  }),
180
177
  ]);