@oh-my-pi/pi-natives 9.1.1 → 9.2.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/package.json +2 -2
- package/src/grep/file-reader.ts +51 -0
- package/src/grep/index.ts +23 -8
- package/src/grep/worker.ts +14 -7
- package/src/highlight/index.ts +4 -0
- package/src/html/index.ts +8 -1
- package/src/image/index.ts +8 -1
- package/src/pool.ts +18 -4
- package/src/worker-resolver.ts +9 -0
- package/wasm/pi_natives_bg.wasm +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-natives",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
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.
|
|
33
|
+
"@oh-my-pi/pi-utils": "9.2.0"
|
|
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
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/grep/worker.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
113
|
-
|
|
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
|
|
package/src/highlight/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|
package/src/image/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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
|
+
}
|
package/wasm/pi_natives_bg.wasm
CHANGED
|
Binary file
|