@oh-my-pi/pi-natives 9.1.1 → 9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-natives",
3
- "version": "9.1.1",
3
+ "version": "9.2.1",
4
4
  "description": "Native Rust functionality compiled to WebAssembly via wasm-bindgen",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -30,7 +30,7 @@
30
30
  "directory": "packages/natives"
31
31
  },
32
32
  "dependencies": {
33
- "@oh-my-pi/pi-utils": "9.1.1"
33
+ "@oh-my-pi/pi-utils": "9.2.1"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^25.0.10"
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Cross-platform file reader for grep.
3
+ *
4
+ * Uses mmap for files <= 4MB on platforms that support it,
5
+ * falls back to reading into a reusable buffer otherwise.
6
+ */
7
+ import * as fs from "node:fs/promises";
8
+
9
+ const MAX_MMAP_SIZE = 4 * 1024 * 1024; // 4MB
10
+ export class FileReader {
11
+ #buffer: Buffer | null = null;
12
+ constructor(private readonly maxSize: number = MAX_MMAP_SIZE) {}
13
+ #getBuffer(size: number): Buffer {
14
+ if (!this.#buffer) {
15
+ this.#buffer = Buffer.allocUnsafe(this.maxSize);
16
+ }
17
+ return this.#buffer.subarray(0, size);
18
+ }
19
+
20
+ async read(filePath: string): Promise<Uint8Array | null> {
21
+ let fileSize: number;
22
+ try {
23
+ const stat = await fs.stat(filePath);
24
+ fileSize = stat.size;
25
+ } catch {
26
+ return null;
27
+ }
28
+
29
+ // Skip files larger than buffer size (only search first 4MB worth)
30
+ const readSize = Math.min(fileSize, this.maxSize);
31
+
32
+ // Try mmap for small files (fast path on Linux/macOS)
33
+ if (fileSize <= this.maxSize) {
34
+ try {
35
+ return Bun.mmap(filePath);
36
+ } catch {
37
+ // mmap not supported (Windows) or failed, fall through to read
38
+ }
39
+ }
40
+
41
+ // Fall back to reading into buffer
42
+ try {
43
+ await using handle = await fs.open(filePath, "r");
44
+ const buffer = this.#getBuffer(readSize);
45
+ const { bytesRead } = await handle.read(buffer, 0, readSize, 0);
46
+ return buffer.subarray(0, bytesRead);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ }
package/src/grep/index.ts CHANGED
@@ -14,6 +14,8 @@ import {
14
14
  search as wasmSearch,
15
15
  } from "../../wasm/pi_natives";
16
16
  import { WorkerPool } from "../pool";
17
+ import { resolveWorkerSpecifier } from "../worker-resolver";
18
+ import { FileReader } from "./file-reader";
17
19
  import { buildGlobPattern, matchesTypeFilter, resolveTypeFilter } from "./filters";
18
20
  import type {
19
21
  ContextLine,
@@ -97,7 +99,17 @@ async function grepDirect(options: GrepOptions, onMatch?: (match: GrepMatch) =>
97
99
  };
98
100
  }
99
101
 
100
- const content = Bun.mmap(searchPath);
102
+ const fileReader = new FileReader();
103
+ const content = await fileReader.read(searchPath);
104
+ if (!content) {
105
+ return {
106
+ matches,
107
+ totalMatches,
108
+ filesWithMatches,
109
+ filesSearched,
110
+ limitReached: limitReached || undefined,
111
+ };
112
+ }
101
113
  filesSearched = 1;
102
114
 
103
115
  const result = compiledPattern.search_bytes(
@@ -148,6 +160,7 @@ async function grepDirect(options: GrepOptions, onMatch?: (match: GrepMatch) =>
148
160
  gitignore: true,
149
161
  });
150
162
 
163
+ const fileReader = new FileReader();
151
164
  for (const relativePath of paths) {
152
165
  if (limitReached) break;
153
166
  if (typeFilter && !matchesTypeFilter(relativePath, typeFilter)) {
@@ -157,12 +170,8 @@ async function grepDirect(options: GrepOptions, onMatch?: (match: GrepMatch) =>
157
170
  const normalizedPath = relativePath.replace(/\\/g, "/");
158
171
  const fullPath = path.join(searchPath, normalizedPath);
159
172
 
160
- let content: Uint8Array;
161
- try {
162
- content = Bun.mmap(fullPath);
163
- } catch {
164
- continue;
165
- }
173
+ const content = await fileReader.read(fullPath);
174
+ if (!content) continue;
166
175
 
167
176
  filesSearched++;
168
177
 
@@ -293,7 +302,13 @@ export async function grepPool(options: GrepOptions): Promise<GrepResult> {
293
302
  // =============================================================================
294
303
 
295
304
  const pool = new WorkerPool<WorkerRequest, WorkerResponse>({
296
- workerUrl: new URL("./worker.ts", import.meta.url).href,
305
+ createWorker: () =>
306
+ new Worker(
307
+ resolveWorkerSpecifier({
308
+ compiled: "./packages/natives/src/grep/worker.ts",
309
+ dev: new URL("./worker.ts", import.meta.url),
310
+ }),
311
+ ),
297
312
  maxWorkers: 4,
298
313
  idleTimeoutMs: 30_000,
299
314
  });
@@ -7,6 +7,7 @@ import * as fs from "node:fs/promises";
7
7
  import * as path from "node:path";
8
8
  import { globPaths } from "@oh-my-pi/pi-utils";
9
9
  import { CompiledPattern } from "../../wasm/pi_natives";
10
+ import { FileReader } from "./file-reader";
10
11
  import { buildGlobPattern, matchesTypeFilter, resolveTypeFilter } from "./filters";
11
12
  import type { GrepMatch, GrepOptions, GrepResult, WasmSearchResult, WorkerRequest, WorkerResponse } from "./types";
12
13
 
@@ -46,6 +47,7 @@ async function runGrep(request: GrepOptions): Promise<GrepResult> {
46
47
  const typeFilter = resolveTypeFilter(request.type);
47
48
  const globPattern = buildGlobPattern(request.glob);
48
49
 
50
+ const fileReader = new FileReader();
49
51
  if (isFile) {
50
52
  if (typeFilter && !matchesTypeFilter(searchPath, typeFilter)) {
51
53
  return {
@@ -57,7 +59,16 @@ async function runGrep(request: GrepOptions): Promise<GrepResult> {
57
59
  };
58
60
  }
59
61
 
60
- const content = Bun.mmap(searchPath);
62
+ const content = await fileReader.read(searchPath);
63
+ if (!content) {
64
+ return {
65
+ matches,
66
+ totalMatches,
67
+ filesWithMatches,
68
+ filesSearched,
69
+ limitReached: limitReached || undefined,
70
+ };
71
+ }
61
72
  filesSearched = 1;
62
73
 
63
74
  const result = compiledPattern.search_bytes(
@@ -109,12 +120,8 @@ async function runGrep(request: GrepOptions): Promise<GrepResult> {
109
120
  const normalizedPath = relativePath.replace(/\\/g, "/");
110
121
  const fullPath = path.join(searchPath, normalizedPath);
111
122
 
112
- let content: Uint8Array;
113
- try {
114
- content = Bun.mmap(fullPath);
115
- } catch {
116
- continue;
117
- }
123
+ const content = await fileReader.read(fullPath);
124
+ if (!content) continue;
118
125
 
119
126
  filesSearched++;
120
127
 
@@ -26,6 +26,10 @@ export interface HighlightColors {
26
26
  type: string;
27
27
  operator: string;
28
28
  punctuation: string;
29
+ /** Color for diff inserted lines (+). Optional, defaults to no coloring. */
30
+ inserted?: string;
31
+ /** Color for diff deleted lines (-). Optional, defaults to no coloring. */
32
+ deleted?: string;
29
33
  }
30
34
 
31
35
  /**
package/src/html/index.ts CHANGED
@@ -5,12 +5,19 @@
5
5
  */
6
6
 
7
7
  import { type RequestOptions, WorkerPool } from "../pool";
8
+ import { resolveWorkerSpecifier } from "../worker-resolver";
8
9
  import type { HtmlRequest, HtmlResponse, HtmlToMarkdownOptions } from "./types";
9
10
 
10
11
  export type { HtmlToMarkdownOptions } from "./types";
11
12
 
12
13
  const pool = new WorkerPool<HtmlRequest, HtmlResponse>({
13
- workerUrl: new URL("./worker.ts", import.meta.url).href,
14
+ createWorker: () =>
15
+ new Worker(
16
+ resolveWorkerSpecifier({
17
+ compiled: "./packages/natives/src/html/worker.ts",
18
+ dev: new URL("./worker.ts", import.meta.url),
19
+ }),
20
+ ),
14
21
  maxWorkers: 2,
15
22
  idleTimeoutMs: 30_000,
16
23
  });
@@ -6,13 +6,20 @@
6
6
  */
7
7
 
8
8
  import { WorkerPool } from "../pool";
9
+ import { resolveWorkerSpecifier } from "../worker-resolver";
9
10
  import type { ImageRequest, ImageResponse } from "./types";
10
11
 
11
12
  // Re-export the enum for filter selection
12
13
  export { SamplingFilter } from "../../wasm/pi_natives";
13
14
 
14
15
  const pool = new WorkerPool<ImageRequest, ImageResponse>({
15
- workerUrl: new URL("./worker.ts", import.meta.url).href,
16
+ createWorker: () =>
17
+ new Worker(
18
+ resolveWorkerSpecifier({
19
+ compiled: "./packages/natives/src/image/worker.ts",
20
+ dev: new URL("./worker.ts", import.meta.url),
21
+ }),
22
+ ),
16
23
  maxWorkers: 1,
17
24
  idleTimeoutMs: 0, // Keep alive - stateful (image handles)
18
25
  });
package/src/pool.ts CHANGED
@@ -19,8 +19,10 @@ export interface BaseResponse {
19
19
  }
20
20
 
21
21
  export interface WorkerPoolOptions {
22
- /** URL to the worker script. */
23
- workerUrl: string | URL;
22
+ /** URL to the worker script (deprecated: use createWorker for compiled binaries). */
23
+ workerUrl?: string | URL;
24
+ /** Factory function to create workers. Required for compiled binaries where Bun needs static analysis. */
25
+ createWorker?: () => Worker;
24
26
  /** Maximum number of workers (default: 4). */
25
27
  maxWorkers?: number;
26
28
  /** Idle timeout in ms before terminating unused workers (0 = never, default: 30000). */
@@ -61,7 +63,14 @@ interface PendingRequest<T> {
61
63
  * @typeParam TRes - Response message type (must extend BaseResponse)
62
64
  */
63
65
  export class WorkerPool<TReq extends BaseRequest, TRes extends BaseResponse> {
64
- readonly #options: Required<WorkerPoolOptions>;
66
+ readonly #options: {
67
+ workerUrl?: string | URL;
68
+ createWorker?: () => Worker;
69
+ maxWorkers: number;
70
+ idleTimeoutMs: number;
71
+ initTimeoutMs: number;
72
+ stuckGracePeriodMs: number;
73
+ };
65
74
  readonly #pool: PooledWorker[] = [];
66
75
  readonly #waiters: Array<(worker: PooledWorker) => void> = [];
67
76
  readonly #pending = new Map<number, PendingRequest<TRes>>();
@@ -69,8 +78,12 @@ export class WorkerPool<TReq extends BaseRequest, TRes extends BaseResponse> {
69
78
  #idleCheckInterval: ReturnType<typeof setInterval> | null = null;
70
79
 
71
80
  constructor(options: WorkerPoolOptions) {
81
+ if (!options.workerUrl && !options.createWorker) {
82
+ throw new Error("WorkerPool requires either workerUrl or createWorker");
83
+ }
72
84
  this.#options = {
73
85
  workerUrl: options.workerUrl,
86
+ createWorker: options.createWorker,
74
87
  maxWorkers: options.maxWorkers ?? 4,
75
88
  idleTimeoutMs: options.idleTimeoutMs ?? 30_000,
76
89
  initTimeoutMs: options.initTimeoutMs ?? 10_000,
@@ -152,7 +165,7 @@ export class WorkerPool<TReq extends BaseRequest, TRes extends BaseResponse> {
152
165
  }
153
166
 
154
167
  #createWorker(): PooledWorker {
155
- const worker = new Worker(this.#options.workerUrl);
168
+ const worker = this.#options.createWorker ? this.#options.createWorker() : new Worker(this.#options.workerUrl!);
156
169
 
157
170
  const pooledWorker: PooledWorker = {
158
171
  worker,
@@ -314,6 +327,7 @@ export class WorkerPool<TReq extends BaseRequest, TRes extends BaseResponse> {
314
327
  dispose: () => clearTimeout(timeout),
315
328
  } as PendingRequest<TRes>);
316
329
 
330
+ pooledWorker.currentRequestId = id;
317
331
  pooledWorker.worker.postMessage({ type: "init", id } satisfies BaseRequest);
318
332
  return promise;
319
333
  }
@@ -0,0 +1,9 @@
1
+ declare const OMP_COMPILED: boolean | undefined;
2
+
3
+ export function resolveWorkerSpecifier(options: { compiled: string; dev: URL }): string | URL {
4
+ if (typeof OMP_COMPILED !== "undefined" && OMP_COMPILED) {
5
+ return options.compiled;
6
+ }
7
+
8
+ return options.dev;
9
+ }
Binary file