@oh-my-pi/pi-utils 18.1.22 → 18.2.1

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/src/stream.ts CHANGED
@@ -4,6 +4,7 @@ import { abortableSource } from "./abortable";
4
4
  import { parseStreamingJson } from "./json-parse";
5
5
 
6
6
  const LF = 0x0a;
7
+ const CR = 0x0d;
7
8
 
8
9
  export async function* readLines(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Uint8Array> {
9
10
  const buffer = new ConcatSink();
@@ -56,13 +57,19 @@ export async function* readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?:
56
57
  }
57
58
  }
58
59
 
59
- // =============================================================================
60
- // SSE (Server-Sent Events)
61
- // =============================================================================
62
-
63
- class ConcatSink {
60
+ /**
61
+ * Amortized byte accumulator for chunked stream readers.
62
+ *
63
+ * Holds the unconsumed tail of a stream in a single growing `Buffer` so that
64
+ * appending N chunks costs O(total bytes) instead of re-copying the whole
65
+ * prefix per chunk. Backs {@link readLines}, {@link readJsonl} and
66
+ * {@link readSseEvents}; also usable directly when a reader needs its own
67
+ * framing loop (see `consume` and `flush`).
68
+ */
69
+ export class ConcatSink {
64
70
  #space?: Buffer;
65
71
  #length = 0;
72
+ #skipLeadingLf = false;
66
73
 
67
74
  #ensureCapacity(size: number): Buffer {
68
75
  const space = this.#space;
@@ -100,11 +107,26 @@ class ConcatSink {
100
107
  return this.#length === 0;
101
108
  }
102
109
 
110
+ /**
111
+ * The buffered bytes as a live view — invalidated by the next `append`,
112
+ * `reset` or `consume`.
113
+ */
103
114
  flush(): Uint8Array | undefined {
104
115
  if (!this.#length) return undefined;
105
116
  return this.#space!.subarray(0, this.#length);
106
117
  }
107
118
 
119
+ /** Drop the first `count` buffered bytes, keeping the remainder. */
120
+ consume(count: number) {
121
+ if (count <= 0) return;
122
+ if (count >= this.#length) {
123
+ this.#length = 0;
124
+ return;
125
+ }
126
+ this.#space!.copyWithin(0, count, this.#length);
127
+ this.#length -= count;
128
+ }
129
+
108
130
  clear() {
109
131
  this.#length = 0;
110
132
  }
@@ -133,19 +155,27 @@ class ConcatSink {
133
155
  }
134
156
 
135
157
  appendAndFlushText(chunk: Uint8Array, decoder: TextDecoder): string | undefined {
136
- const lastNewline = chunk.lastIndexOf(LF);
137
- if (lastNewline === -1) {
138
- this.append(chunk);
158
+ let start = 0;
159
+ if (this.#skipLeadingLf) {
160
+ if (chunk.length === 0) return undefined;
161
+ this.#skipLeadingLf = false;
162
+ if (chunk[0] === LF) start = 1;
163
+ }
164
+
165
+ const lastLineEnd = Math.max(chunk.lastIndexOf(LF), chunk.lastIndexOf(CR));
166
+ if (lastLineEnd < start) {
167
+ if (start < chunk.length) this.append(chunk.subarray(start));
139
168
  return undefined;
140
169
  }
141
170
 
142
- const completeEnd = lastNewline + 1;
171
+ const completeEnd = lastLineEnd + 1;
172
+ this.#skipLeadingLf = chunk[lastLineEnd] === CR && completeEnd === chunk.length;
143
173
  let text: string;
144
174
  if (this.isEmpty) {
145
- const complete = completeEnd === chunk.length ? chunk : chunk.subarray(0, completeEnd);
175
+ const complete = start === 0 && completeEnd === chunk.length ? chunk : chunk.subarray(start, completeEnd);
146
176
  text = decoder.decode(complete);
147
177
  } else {
148
- this.append(completeEnd === chunk.length ? chunk : chunk.subarray(0, completeEnd));
178
+ this.append(chunk.subarray(start, completeEnd));
149
179
  text = decoder.decode(this.flush());
150
180
  this.clear();
151
181
  }
@@ -197,6 +227,10 @@ class ConcatSink {
197
227
  }
198
228
  }
199
229
 
230
+ // =============================================================================
231
+ // SSE (Server-Sent Events)
232
+ // =============================================================================
233
+
200
234
  /**
201
235
  * Stream parsed JSON objects from SSE `data:` lines.
202
236
  *
@@ -236,11 +270,24 @@ function isRecoverableTrailingJson(data: string): boolean {
236
270
  return typeof recovered === "object" && recovered !== null;
237
271
  }
238
272
 
239
- export async function* readSseJson<T>(
273
+ /**
274
+ * One dispatched `data:` frame from {@link readSseFrames}: either the parsed JSON
275
+ * value, or the text of a frame `JSON.parse` rejected together with the
276
+ * `SyntaxError` it raised (so the strict reader can rethrow it unchanged).
277
+ */
278
+ type SseFrame<T> = { ok: true; value: T } | { ok: false; raw: string; error: SyntaxError };
279
+
280
+ /**
281
+ * Shared `data:`-line framing for {@link readSseJson} and
282
+ * {@link readSseJsonOrText}: skips empty events, stops at the OpenAI `[DONE]`
283
+ * sentinel, notifies the diagnostic observer, and treats a container-shaped
284
+ * stream tail as a clean end of iteration.
285
+ */
286
+ async function* readSseFrames<T>(
240
287
  stream: ReadableStream<Uint8Array>,
241
288
  signal?: AbortSignal,
242
289
  onEvent?: SseEventObserver,
243
- ): AsyncGenerator<T> {
290
+ ): AsyncGenerator<SseFrame<T>> {
244
291
  for await (const sse of readSseEvents(stream, signal)) {
245
292
  const isTrailing = trailingEvents.has(sse);
246
293
  notifySseEventObserver(onEvent, sse);
@@ -250,16 +297,60 @@ export async function* readSseJson<T>(
250
297
  continue;
251
298
  }
252
299
  try {
253
- yield JSON.parse(data) as T;
300
+ yield { ok: true, value: JSON.parse(data) as T };
254
301
  } catch (err) {
255
302
  if (err instanceof SyntaxError && isTrailing && isRecoverableTrailingJson(data)) {
256
303
  return;
257
304
  }
305
+ if (err instanceof SyntaxError) {
306
+ yield { ok: false, raw: data, error: err };
307
+ continue;
308
+ }
258
309
  throw err;
259
310
  }
260
311
  }
261
312
  }
262
313
 
314
+ export async function* readSseJson<T>(
315
+ stream: ReadableStream<Uint8Array>,
316
+ signal?: AbortSignal,
317
+ onEvent?: SseEventObserver,
318
+ ): AsyncGenerator<T> {
319
+ for await (const frame of readSseFrames<T>(stream, signal, onEvent)) {
320
+ if (!frame.ok) throw frame.error;
321
+ yield frame.value;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Like {@link readSseJson}, but a `data:` frame that is not valid JSON is yielded
327
+ * as its raw text instead of raising a `SyntaxError`. Cut-off container-shaped
328
+ * stream tails stay recoverable, exactly as they are in {@link readSseJson}.
329
+ *
330
+ * Consumers that only understand objects must treat a `string` yield as a
331
+ * transport-level failure (for example a `429 Too Many Requests` or an HTML
332
+ * throttle page from a reverse proxy that already committed to the stream). This
333
+ * exists because `readSseJson`'s baseline consumers span unrelated transports
334
+ * whose error handling a text yield would subtly change; new call sites opt in.
335
+ *
336
+ * Note that the text lane is only the frames `JSON.parse` *rejected*: a frame
337
+ * carrying a JSON-encoded string (`data: "429 Too Many Requests"`) parses, so it
338
+ * is yielded as that string and is indistinguishable from a rejected frame by
339
+ * type alone. Consumers branching on `typeof === "string"` therefore see both,
340
+ * which is the safe direction — each is classified as text rather than trusted as
341
+ * an event object.
342
+ */
343
+ export async function* readSseJsonOrText<T>(
344
+ stream: ReadableStream<Uint8Array>,
345
+ signal?: AbortSignal,
346
+ onEvent?: SseEventObserver,
347
+ ): AsyncGenerator<T | string> {
348
+ for await (const frame of readSseFrames<T>(stream, signal, onEvent)) {
349
+ if (!frame.ok) yield frame.raw;
350
+ else yield frame.value;
351
+ }
352
+ }
353
+
263
354
  /**
264
355
  * A single Server-Sent Event dispatched on a blank-line boundary.
265
356
  *
@@ -294,7 +385,7 @@ interface SseEventState {
294
385
  }
295
386
 
296
387
  // Complete lines are decoded in one batch per source chunk. Each batch ends on
297
- // LF, which cannot split a multi-byte UTF-8 sequence.
388
+ // an ASCII line-ending byte, which cannot split a multi-byte UTF-8 sequence.
298
389
  const SSE_DECODER = new TextDecoder("utf-8");
299
390
 
300
391
  function flushSseEvent(state: SseEventState): ServerSentEvent | null {
@@ -318,11 +409,6 @@ function flushSseEvent(state: SseEventState): ServerSentEvent | null {
318
409
  }
319
410
 
320
411
  function pushSseLine(line: string, state: SseEventState): ServerSentEvent | null {
321
- // Complete-line batches split on LF only; strip a trailing CR so CRLF sources
322
- // don't leak `\r` into field values.
323
- if (line.charCodeAt(line.length - 1) === 0x0d /* '\r' */) {
324
- line = line.slice(0, -1);
325
- }
326
412
  if (line.length === 0) return flushSseEvent(state);
327
413
 
328
414
  // Comment line: keep in `raw` for diagnostic context, skip parsing.
@@ -398,13 +484,21 @@ export async function* readSseEvents(
398
484
  if (text === undefined) continue;
399
485
  let start = 0;
400
486
  while (start < text.length) {
401
- const newline = text.indexOf("\n", start);
402
- const event = pushSseLine(text.slice(start, newline), state);
487
+ let lineEnd = start;
488
+ while (lineEnd < text.length) {
489
+ const code = text.charCodeAt(lineEnd);
490
+ if (code === LF || code === CR) break;
491
+ lineEnd++;
492
+ }
493
+ const event = pushSseLine(text.slice(start, lineEnd), state);
403
494
  if (event) yield event;
404
- start = newline + 1;
495
+ if (text.charCodeAt(lineEnd) === CR && text.charCodeAt(lineEnd + 1) === LF) {
496
+ lineEnd++;
497
+ }
498
+ start = lineEnd + 1;
405
499
  }
406
500
  }
407
- // Treat any trailing partial line (no terminating LF) as a complete line.
501
+ // Treat any trailing partial line (no terminating line ending) as complete.
408
502
  if (!lineBuffer.isEmpty) {
409
503
  const tail = lineBuffer.flush();
410
504
  if (tail) {
package/src/which.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  import * as fs from "node:fs";
12
12
  import * as os from "node:os";
13
13
  import * as path from "node:path";
14
+ import { isFullyQualifiedPath } from "./path";
14
15
 
15
16
  type CacheKey = string | bigint | number;
16
17
 
@@ -179,6 +180,12 @@ export interface WhichOptions extends Bun.WhichOptions {
179
180
  * Defaults to `WhichCachePolicy.Fresh`.
180
181
  */
181
182
  cache?: WhichCachePolicy;
183
+ /**
184
+ * Only search absolute directory entries in PATH, ignoring relative entries
185
+ * (e.g. `.` or `./bin`) and empty components to prevent resolving against
186
+ * an untrusted working directory.
187
+ */
188
+ requireAbsolutePaths?: boolean;
182
189
  }
183
190
 
184
191
  // Darwin-specific "which" shim: consult Xcode/CLT toolchain directories after $PATH.
@@ -192,6 +199,15 @@ function darwinWhich(command: string, options?: Bun.WhichOptions): string | null
192
199
  return null;
193
200
  }
194
201
 
202
+ function filterAbsoluteSearchPath(rawPath: string | undefined): string | null {
203
+ if (!rawPath) return null;
204
+ const safePath = rawPath
205
+ .split(path.delimiter)
206
+ .filter(dir => dir.length > 0 && isFullyQualifiedPath(dir))
207
+ .join(path.delimiter);
208
+ return safePath || null;
209
+ }
210
+
195
211
  // Which function that incorporates Darwin Xcode logic if platform reports as 'darwin'.
196
212
  // Look `Bun.which` up per call rather than capturing it at import, so a `Bun.which`
197
213
  // stub installed later (the per-test seam) is honoured on every platform.
@@ -219,8 +235,15 @@ function cacheKey(command: string, options?: Bun.WhichOptions): CacheKey {
219
235
  */
220
236
  export function $which(command: string, options?: WhichOptions): string | null {
221
237
  const cachePolicy = options?.cache ?? WhichCachePolicy.Cached;
222
- const lookupOptions =
238
+ let lookupOptions =
223
239
  options?.PATH !== undefined || process.env.PATH === undefined ? options : { ...options, PATH: process.env.PATH };
240
+
241
+ if (options?.requireAbsolutePaths) {
242
+ const safePath = filterAbsoluteSearchPath(lookupOptions?.PATH);
243
+ if (!safePath) return null;
244
+ lookupOptions = { ...lookupOptions, PATH: safePath };
245
+ }
246
+
224
247
  let key: CacheKey | undefined;
225
248
 
226
249
  if (cachePolicy !== WhichCachePolicy.Bypass) {
@@ -232,6 +255,9 @@ export function $which(command: string, options?: WhichOptions): string | null {
232
255
  }
233
256
 
234
257
  const result = whichFresh(command, lookupOptions);
258
+ if (result && options?.requireAbsolutePaths && !isFullyQualifiedPath(result)) {
259
+ return null;
260
+ }
235
261
  if (key != null && cachePolicy !== WhichCachePolicy.ReadOnly) {
236
262
  toolCache.set(key, result);
237
263
  }
package/src/xml.ts CHANGED
@@ -104,6 +104,12 @@ class XmlReader {
104
104
  this.#readProcessingInstruction(document);
105
105
  continue;
106
106
  }
107
+ if (this.#xml.startsWith("</", this.#position)) {
108
+ // Stray end tag with no element open: skip it instead of parsing it as a new element,
109
+ // whose empty name would leave #readElement stuck on the `/`.
110
+ this.#skipThrough(">");
111
+ continue;
112
+ }
107
113
  if (this.#xml[this.#position] === "<") {
108
114
  const element = this.#readElement("");
109
115
  this.#addValue(document, element.name, element.name, element.value, element.leaf, null);
@@ -138,6 +144,11 @@ class XmlReader {
138
144
  this.#position++;
139
145
  this.#skipWhitespace();
140
146
  value = this.#readAttributeValue();
147
+ } else if (attributeName === "") {
148
+ // Markup #readName cannot consume (the stray `/` in `<a / >`): skip the character
149
+ // so the loop always makes progress.
150
+ this.#position++;
151
+ continue;
141
152
  }
142
153
  attributes.push([attributeName, value]);
143
154
  }
@@ -153,6 +164,11 @@ class XmlReader {
153
164
  this.#position = close < 0 ? this.#xml.length : close + 1;
154
165
  break;
155
166
  }
167
+ if (this.#xml.startsWith("</", this.#position)) {
168
+ // End tag for an ancestor (an unclosed `<br>` inside `<p>`, or plain mismatched
169
+ // markup): close this element implicitly and leave the tag to its owner.
170
+ break;
171
+ }
156
172
  if (this.#xml.startsWith("<![CDATA[", this.#position)) {
157
173
  const end = this.#xml.indexOf("]]>", this.#position + 9);
158
174
  const raw = this.#xml.slice(this.#position + 9, end < 0 ? this.#xml.length : end);
@@ -1,28 +0,0 @@
1
- export interface GlobPathsOptions {
2
- /** Base directory for glob patterns. Defaults to getProjectDir(). */
3
- cwd?: string;
4
- /** Glob exclusion patterns. */
5
- exclude?: string[];
6
- /** Abort signal to cancel the glob. */
7
- signal?: AbortSignal;
8
- /** Timeout in milliseconds for the glob operation. */
9
- timeoutMs?: number;
10
- /** Include dotfiles when true. */
11
- dot?: boolean;
12
- /** Only return files (skip directories). Default: true. */
13
- onlyFiles?: boolean;
14
- /** Respect .gitignore files when true. Walks up directory tree to find all applicable .gitignore files. */
15
- gitignore?: boolean;
16
- }
17
- /**
18
- * Load .gitignore patterns from a directory and its parents.
19
- * Walks up the directory tree to find all applicable .gitignore files.
20
- * Returns glob-compatible exclude patterns.
21
- */
22
- export declare function loadGitignorePatterns(baseDir: string): Promise<string[]>;
23
- /**
24
- * Resolve filesystem paths matching glob patterns with optional exclude filters.
25
- * Returns paths relative to the provided cwd (or getProjectDir()).
26
- * Errors and abort/timeouts are surfaced to the caller.
27
- */
28
- export declare function globPaths(patterns: string | string[], options?: GlobPathsOptions): Promise<string[]>;
package/src/glob.ts DELETED
@@ -1,189 +0,0 @@
1
- import * as path from "node:path";
2
- import { Glob } from "bun";
3
- import { getProjectDir } from "./dirs";
4
-
5
- export interface GlobPathsOptions {
6
- /** Base directory for glob patterns. Defaults to getProjectDir(). */
7
- cwd?: string;
8
- /** Glob exclusion patterns. */
9
- exclude?: string[];
10
- /** Abort signal to cancel the glob. */
11
- signal?: AbortSignal;
12
- /** Timeout in milliseconds for the glob operation. */
13
- timeoutMs?: number;
14
- /** Include dotfiles when true. */
15
- dot?: boolean;
16
- /** Only return files (skip directories). Default: true. */
17
- onlyFiles?: boolean;
18
- /** Respect .gitignore files when true. Walks up directory tree to find all applicable .gitignore files. */
19
- gitignore?: boolean;
20
- }
21
-
22
- /** Patterns always excluded (.git is never useful in glob results). */
23
- const ALWAYS_IGNORED = ["**/.git", "**/.git/**"];
24
-
25
- /** node_modules exclusion patterns (skipped if pattern explicitly references node_modules). */
26
- const NODE_MODULES_IGNORED = ["**/node_modules", "**/node_modules/**"];
27
-
28
- /**
29
- * Parse a single .gitignore file and return glob-compatible exclude patterns.
30
- * @param content - Raw content of the .gitignore file
31
- * @param gitignoreDir - Absolute path to the directory containing the .gitignore
32
- * @param baseDir - Absolute path to the glob's cwd (for relativizing rooted patterns)
33
- */
34
- function parseGitignorePatterns(content: string, gitignoreDir: string, baseDir: string): string[] {
35
- const patterns: string[] = [];
36
-
37
- for (const rawLine of content.split("\n")) {
38
- const line = rawLine.trim();
39
- // Skip empty lines and comments
40
- if (!line || line.startsWith("#")) {
41
- continue;
42
- }
43
- // Skip negation patterns (unsupported for simple exclude)
44
- if (line.startsWith("!")) {
45
- continue;
46
- }
47
-
48
- let pattern = line;
49
-
50
- // Handle trailing slash (directory-only match)
51
- // For glob exclude, we treat it as matching the dir and its contents
52
- const isDirectoryOnly = pattern.endsWith("/");
53
- if (isDirectoryOnly) {
54
- pattern = pattern.slice(0, -1);
55
- }
56
-
57
- // Handle rooted patterns (start with /)
58
- if (pattern.startsWith("/")) {
59
- // Rooted pattern: relative to the .gitignore location
60
- const absolutePattern = path.join(gitignoreDir, pattern.slice(1));
61
- const relativeToBase = path.relative(baseDir, absolutePattern);
62
- if (relativeToBase.startsWith("..")) {
63
- // Pattern is outside the search directory, skip
64
- continue;
65
- }
66
- pattern = relativeToBase.replace(/\\/g, "/");
67
- if (isDirectoryOnly) {
68
- patterns.push(pattern);
69
- patterns.push(`${pattern}/**`);
70
- } else {
71
- patterns.push(pattern);
72
- }
73
- } else {
74
- // Unrooted pattern: match anywhere in the tree
75
- if (pattern.includes("/")) {
76
- // Contains slash: match from any directory level
77
- patterns.push(`**/${pattern}`);
78
- if (isDirectoryOnly) {
79
- patterns.push(`**/${pattern}/**`);
80
- }
81
- } else {
82
- // No slash: match file/dir name anywhere
83
- patterns.push(`**/${pattern}`);
84
- if (isDirectoryOnly) {
85
- patterns.push(`**/${pattern}/**`);
86
- }
87
- }
88
- }
89
- }
90
-
91
- return patterns;
92
- }
93
-
94
- /**
95
- * Load .gitignore patterns from a directory and its parents.
96
- * Walks up the directory tree to find all applicable .gitignore files.
97
- * Returns glob-compatible exclude patterns.
98
- */
99
- export async function loadGitignorePatterns(baseDir: string): Promise<string[]> {
100
- const patterns: string[] = [];
101
- const absoluteBase = path.resolve(baseDir);
102
-
103
- let current = absoluteBase;
104
- const maxDepth = 50; // Prevent infinite loops
105
-
106
- for (let i = 0; i < maxDepth; i++) {
107
- const gitignorePath = path.join(current, ".gitignore");
108
-
109
- try {
110
- const content = await Bun.file(gitignorePath).text();
111
- const filePatterns = parseGitignorePatterns(content, current, absoluteBase);
112
- patterns.push(...filePatterns);
113
- } catch {
114
- // .gitignore doesn't exist or can't be read, continue
115
- }
116
-
117
- const parent = path.dirname(current);
118
- if (parent === current) {
119
- // Reached filesystem root
120
- break;
121
- }
122
- current = parent;
123
- }
124
-
125
- return patterns;
126
- }
127
-
128
- /**
129
- * Resolve filesystem paths matching glob patterns with optional exclude filters.
130
- * Returns paths relative to the provided cwd (or getProjectDir()).
131
- * Errors and abort/timeouts are surfaced to the caller.
132
- */
133
- export async function globPaths(patterns: string | string[], options: GlobPathsOptions = {}): Promise<string[]> {
134
- const { cwd, exclude, signal, timeoutMs, dot, onlyFiles = true, gitignore } = options;
135
-
136
- // Build exclude list: always exclude .git, exclude node_modules unless pattern references it
137
- const patternArray = Array.isArray(patterns) ? patterns : [patterns];
138
- const mentionsNodeModules = patternArray.some(p => p.includes("node_modules"));
139
-
140
- const baseExclude = mentionsNodeModules ? [...ALWAYS_IGNORED] : [...ALWAYS_IGNORED, ...NODE_MODULES_IGNORED];
141
- let effectiveExclude = exclude ? [...baseExclude, ...exclude] : baseExclude;
142
-
143
- if (gitignore) {
144
- const gitignorePatterns = await loadGitignorePatterns(cwd ?? getProjectDir());
145
- effectiveExclude = [...effectiveExclude, ...gitignorePatterns];
146
- }
147
-
148
- const base = cwd ?? getProjectDir();
149
- const allResults: string[] = [];
150
-
151
- // Combine timeout and abort signals
152
- const timeoutSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined;
153
- const combinedSignal =
154
- signal && timeoutSignal ? AbortSignal.any([signal, timeoutSignal]) : (signal ?? timeoutSignal);
155
-
156
- for (const pattern of patternArray) {
157
- const glob = new Glob(pattern);
158
- const scanOptions = {
159
- cwd: base,
160
- dot,
161
- onlyFiles,
162
- throwErrorOnBrokenSymlink: false,
163
- };
164
-
165
- for await (const entry of glob.scan(scanOptions)) {
166
- if (combinedSignal?.aborted) {
167
- const reason = combinedSignal.reason;
168
- if (reason instanceof Error) throw reason;
169
- throw new DOMException("Aborted", "AbortError");
170
- }
171
-
172
- // Check exclusion patterns
173
- const normalized = entry.replace(/\\/g, "/");
174
- let excluded = false;
175
- for (const excludePattern of effectiveExclude) {
176
- const excludeGlob = new Glob(excludePattern);
177
- if (excludeGlob.match(normalized)) {
178
- excluded = true;
179
- break;
180
- }
181
- }
182
- if (!excluded) {
183
- allResults.push(normalized);
184
- }
185
- }
186
- }
187
-
188
- return allResults;
189
- }