castrum 0.7.0 → 0.9.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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -368
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +2763 -0
  5. package/dist/index.js +8707 -0
  6. package/docs/API.md +337 -0
  7. package/docs/ARCHITECTURE.md +507 -0
  8. package/docs/BENCHMARKS.md +343 -0
  9. package/docs/CASE_STUDY.md +240 -0
  10. package/docs/ENVIRONMENT.md +81 -0
  11. package/docs/FFI_BUN_GUIDE.md +327 -0
  12. package/docs/GETTING_STARTED.md +192 -0
  13. package/docs/INGRESS-ROUTER.md +81 -0
  14. package/docs/INGRESS.md +746 -0
  15. package/docs/NATIVE-ROUTE.md +53 -0
  16. package/docs/REPO_MAP.md +279 -0
  17. package/docs/adr/0001-two-wire-formats.md +39 -0
  18. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  19. package/docs/adr/0003-bun-builtins-delegation.md +43 -0
  20. package/docs/bun-builtins-decision-matrix.md +115 -0
  21. package/index.ts +44 -8
  22. package/package.json +98 -33
  23. package/src/ingress/body.ts +289 -0
  24. package/src/ingress/constants.ts +220 -48
  25. package/src/ingress/context.ts +136 -0
  26. package/src/ingress/decode/baked-result.ts +121 -0
  27. package/src/ingress/decode/fast-result.ts +160 -0
  28. package/src/ingress/decode/packed-sections.ts +56 -0
  29. package/src/ingress/decode/result-base.ts +172 -0
  30. package/src/ingress/errors.ts +72 -0
  31. package/src/ingress/fast.ts +152 -1021
  32. package/src/ingress/handlers.ts +639 -1001
  33. package/src/ingress/headers/baked-templates.ts +102 -0
  34. package/src/ingress/headers/cors.ts +44 -0
  35. package/src/ingress/headers/fast-templates.ts +238 -0
  36. package/src/ingress/headers/hsts.ts +83 -0
  37. package/src/ingress/health.ts +68 -0
  38. package/src/ingress/index.ts +142 -405
  39. package/src/ingress/metrics.ts +132 -0
  40. package/src/ingress/options.ts +197 -0
  41. package/src/ingress/packing/gather-raw-headers.ts +139 -0
  42. package/src/ingress/packing/header-packing.ts +75 -0
  43. package/src/ingress/packing/input-packer.ts +164 -0
  44. package/src/ingress/packing/scratch.ts +50 -0
  45. package/src/ingress/packing/select-headers.ts +102 -0
  46. package/src/ingress/response/error-bodies.ts +75 -0
  47. package/src/ingress/response/terminal.ts +59 -0
  48. package/src/ingress/router.ts +237 -0
  49. package/src/ingress/routes/common.ts +79 -0
  50. package/src/ingress/routes/delete.ts +14 -0
  51. package/src/ingress/routes/echo.ts +107 -0
  52. package/src/ingress/routes/fallback.ts +23 -0
  53. package/src/ingress/routes/head.ts +27 -0
  54. package/src/ingress/routes/index.ts +11 -0
  55. package/src/ingress/routes/json-write.ts +129 -0
  56. package/src/ingress/routes/options.ts +26 -0
  57. package/src/ingress/routes/read.ts +35 -0
  58. package/src/ingress/routes/responder.ts +159 -0
  59. package/src/ingress/server-node.ts +376 -0
  60. package/src/ingress/server.ts +475 -0
  61. package/src/ingress/shared.ts +132 -0
  62. package/src/ingress/status.ts +97 -0
  63. package/src/ingress/types.ts +191 -0
  64. package/src/integration/batch.ts +58 -0
  65. package/src/integration/index.ts +29 -0
  66. package/src/integration/pipeline.ts +220 -0
  67. package/src/integration/streaming.ts +78 -0
  68. package/src/integration/websocket.ts +93 -0
  69. package/src/loader/batch.ts +137 -0
  70. package/src/loader/cost.ts +105 -0
  71. package/src/loader/index.ts +745 -0
  72. package/src/loader/ops.ts +657 -0
  73. package/src/loader/state.ts +84 -0
  74. package/src/loader/types.ts +145 -0
  75. package/src/native/ffi/constants.ts +58 -0
  76. package/src/native/ffi/selftest.ts +729 -0
  77. package/src/native/ffi/types.ts +474 -0
  78. package/src/native/ffi.ts +1775 -0
  79. package/src/native/index.ts +8 -207
  80. package/src/native/loader.ts +240 -0
  81. package/src/native/types/instances.ts +274 -0
  82. package/src/native/types.ts +401 -0
  83. package/src/runtime/builtins.ts +131 -0
  84. package/src/runtime/codec.ts +147 -0
  85. package/src/runtime/detect.ts +36 -0
  86. package/src/runtime/index.ts +28 -0
  87. package/src/runtime/native.ts +35 -0
  88. package/src/runtime/server.ts +31 -0
  89. package/src/runtime/transport.ts +64 -0
  90. package/src/runtime/types.ts +173 -0
  91. package/src/runtime/uuid.ts +45 -0
  92. package/src/runtime/websocket.ts +30 -0
  93. package/src/rust-ffi/addon.ts +10 -0
  94. package/src/rust-ffi/batch/build.ts +155 -0
  95. package/src/rust-ffi/batch/index.ts +8 -0
  96. package/src/rust-ffi/batch/types.ts +92 -0
  97. package/src/rust-ffi/client.ts +87 -0
  98. package/src/rust-ffi/context.ts +193 -0
  99. package/src/rust-ffi/index.ts +51 -541
  100. package/src/rust-ffi/into.ts +21 -0
  101. package/src/rust-ffi/options.ts +113 -0
  102. package/src/rust-ffi/packed.ts +253 -0
  103. package/src/rust-ffi/proven.ts +28 -0
  104. package/src/rust-ffi/scalar/crypto.ts +346 -0
  105. package/src/rust-ffi/scalar/factories.ts +459 -0
  106. package/src/rust-ffi/scalar/hashing.ts +74 -0
  107. package/src/rust-ffi/scalar/http.ts +337 -0
  108. package/src/rust-ffi/scalar/index.ts +31 -0
  109. package/src/rust-ffi/scalar/interface.ts +325 -0
  110. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  111. package/src/rust-ffi/scalar/json.ts +61 -0
  112. package/src/rust-ffi/scalar/payload.ts +241 -0
  113. package/src/rust-ffi/text.ts +121 -0
  114. package/src/selection.json +194 -0
  115. package/src/selection.ts +73 -0
  116. package/src/shared/adaptive.ts +67 -0
  117. package/src/shared/buffer-pool.ts +264 -0
  118. package/src/shared/bytes.ts +108 -9
  119. package/src/shared/codec.ts +78 -0
  120. package/src/shared/env.ts +20 -0
  121. package/src/shared/log.ts +75 -0
  122. package/src/shared/metrics.ts +247 -0
  123. package/src/shared/packed/index.ts +10 -0
  124. package/src/shared/packed/parsers.ts +31 -0
  125. package/src/shared/packed/schema.ts +23 -0
  126. package/src/shared/packed/wire.ts +504 -0
  127. package/src/shared/proven.ts +289 -0
  128. package/src/shared/request-id.ts +67 -0
  129. package/src/shared/response.ts +104 -0
  130. package/src/shared/runtime.ts +29 -0
  131. package/src/shared/trace.ts +81 -0
  132. package/src/shared/uuid.ts +15 -0
  133. package/Cargo.toml +0 -73
  134. package/bench.ts +0 -3
  135. package/rust/batch.rs +0 -195
  136. package/rust/cookie_parser.rs +0 -70
  137. package/rust/cors.rs +0 -263
  138. package/rust/hashing.rs +0 -164
  139. package/rust/headers.rs +0 -234
  140. package/rust/hmac_sha256.rs +0 -113
  141. package/rust/http_parser.rs +0 -132
  142. package/rust/ingress.rs +0 -616
  143. package/rust/ingress_constants.rs +0 -89
  144. package/rust/ip_trust.rs +0 -185
  145. package/rust/json_ops.rs +0 -66
  146. package/rust/json_patch_ops.rs +0 -25
  147. package/rust/json_schema.rs +0 -151
  148. package/rust/json_ser.rs +0 -429
  149. package/rust/lib.rs +0 -48
  150. package/rust/method.rs +0 -141
  151. package/rust/mime_lookup.rs +0 -76
  152. package/rust/output.rs +0 -238
  153. package/rust/proxy.rs +0 -90
  154. package/rust/query_parser.rs +0 -141
  155. package/rust/random_token.rs +0 -52
  156. package/rust/rate_limit.rs +0 -203
  157. package/rust/terminal.rs +0 -95
  158. package/rust/unit_tests.rs +0 -843
  159. package/rust/url_codec.rs +0 -133
  160. package/rust/util.rs +0 -537
  161. package/rust/validation.rs +0 -107
  162. package/rust/websocket.rs +0 -23
  163. package/src/baseline/index.ts +0 -12
  164. package/src/baseline/tasks/cookie.ts +0 -22
  165. package/src/baseline/tasks/hashing.ts +0 -18
  166. package/src/baseline/tasks/hmac.ts +0 -38
  167. package/src/baseline/tasks/http.ts +0 -62
  168. package/src/baseline/tasks/json-patch.ts +0 -12
  169. package/src/baseline/tasks/json.ts +0 -50
  170. package/src/baseline/tasks/mime.ts +0 -5
  171. package/src/baseline/tasks/query.ts +0 -14
  172. package/src/baseline/tasks/token.ts +0 -15
  173. package/src/baseline/tasks/url.ts +0 -11
  174. package/src/baseline/tasks/validation.ts +0 -19
  175. package/src/baseline/tasks/websocket.ts +0 -14
  176. package/src/bench/assert.ts +0 -70
  177. package/src/bench/checks.ts +0 -227
  178. package/src/bench/checksum.ts +0 -27
  179. package/src/bench/comparisons.ts +0 -70
  180. package/src/bench/concurrent-worker.ts +0 -135
  181. package/src/bench/fixtures.ts +0 -233
  182. package/src/bench/index.ts +0 -2
  183. package/src/bench/measure.ts +0 -177
  184. package/src/bench/now.ts +0 -3
  185. package/src/bench/report.ts +0 -79
  186. package/src/bench/run.ts +0 -57
  187. package/src/bench/tasks/complex.ts +0 -232
  188. package/src/bench/tasks/concurrent.ts +0 -213
  189. package/src/bench/tasks/cookie.ts +0 -44
  190. package/src/bench/tasks/hashing.ts +0 -33
  191. package/src/bench/tasks/hmac.ts +0 -36
  192. package/src/bench/tasks/http.ts +0 -50
  193. package/src/bench/tasks/index.ts +0 -52
  194. package/src/bench/tasks/json-patch.ts +0 -21
  195. package/src/bench/tasks/json.ts +0 -33
  196. package/src/bench/tasks/mime.ts +0 -21
  197. package/src/bench/tasks/query.ts +0 -44
  198. package/src/bench/tasks/stress.ts +0 -142
  199. package/src/bench/tasks/token.ts +0 -20
  200. package/src/bench/tasks/url.ts +0 -34
  201. package/src/bench/tasks/validation.ts +0 -57
  202. package/src/bench/tasks/websocket.ts +0 -21
  203. package/src/bench/types.ts +0 -60
  204. package/src/data/json-rows.ts +0 -33
  205. package/src/ingress/packed-input.ts +0 -59
  206. package/src/shared/json.ts +0 -18
  207. package/src/shared/packed.ts +0 -316
@@ -1,207 +1,8 @@
1
- import { existsSync } from "node:fs";
2
- import { fileURLToPath } from "node:url";
3
- import { join, dirname } from "node:path";
4
- import { createRequire } from "node:module";
5
-
6
- const require = createRequire(import.meta.url);
7
-
8
- export interface IngressInstance {
9
- handleRequestPacked(
10
- input: Uint8Array,
11
- body: Uint8Array | null,
12
- output: Uint8Array,
13
- ): number;
14
-
15
- handleRequestFullSync(
16
- methodKind: number,
17
- url: string,
18
- ip: string,
19
- requestId: string,
20
- headers: Array<[string, string]>,
21
- body: Uint8Array | null,
22
- outputBufferSize?: number,
23
- ): Uint8Array;
24
- }
25
-
26
- function resolveAddonPath(): string {
27
- const __dirname = dirname(fileURLToPath(import.meta.url));
28
- const platform = process.platform;
29
- const arch = process.arch;
30
- const base = join(__dirname, "..", "..");
31
-
32
- // napi-rs artifact naming: castrum.<platform>-<arch>[-<libc>].node
33
- // e.g. linux-x64-gnu, linux-x64-musl, win32-x64-msvc, darwin-arm64.
34
- const libcVariants =
35
- platform === "win32"
36
- ? ["msvc", "gnu"]
37
- : platform === "linux"
38
- ? ["gnu", "musl"]
39
- : [""];
40
-
41
- const names = libcVariants.map(
42
- (libc) => `castrum.${platform}-${arch}${libc ? `-${libc}` : ""}.node`,
43
- );
44
- names.push("castrum.node");
45
-
46
- const roots = [base, join(base, "target", "release")];
47
-
48
- const candidates: string[] = [];
49
- for (const root of roots) {
50
- for (const name of names) {
51
- candidates.push(join(root, name));
52
- }
53
- }
54
-
55
- for (const candidate of candidates) {
56
- if (existsSync(candidate)) {
57
- return candidate;
58
- }
59
- }
60
-
61
- throw new Error(
62
- `Could not find castrum native addon.\n` +
63
- `Run: bun run build\n` +
64
- `Looked in:\n${candidates.map((c) => ` - ${c}`).join("\n")}`,
65
- );
66
- }
67
-
68
- interface NativeAddon {
69
- HmacSigner: new (key: Uint8Array) => HmacSignerInstance;
70
- Ingress: new (options: Record<string, unknown>) => IngressInstance;
71
-
72
- // ── Ingress binary-layout constants (single source of truth: rust/ingress_constants.rs) ──
73
- INGRESS_OUT_VERDICT: number;
74
- INGRESS_OUT_ERROR_CODE: number;
75
- INGRESS_OUT_STATUS: number;
76
- INGRESS_OUT_FLAGS: number;
77
- INGRESS_OUT_RATE_LIMIT: number;
78
- INGRESS_OUT_RATE_REMAINING: number;
79
- INGRESS_OUT_RATE_RESET: number;
80
- INGRESS_OUT_RETRY_AFTER: number;
81
- INGRESS_OUT_COOKIES_JSON_LEN: number;
82
- INGRESS_OUT_QUERY_JSON_LEN: number;
83
- INGRESS_OUT_HEADER_VARIANT: number;
84
- INGRESS_OUT_BODY_JSON_LEN: number;
85
- INGRESS_OUT_DATA_START: number;
86
- INGRESS_FLAG_HAS_COOKIES: number;
87
- INGRESS_FLAG_HAS_QUERY: number;
88
- INGRESS_FLAG_BODY_VALID_JSON: number;
89
- INGRESS_FLAG_SCHEMA_VALID: number;
90
- INGRESS_FLAG_CORS_ALLOWED: number;
91
- INGRESS_FLAG_IS_PREFLIGHT: number;
92
- INGRESS_FLAG_RATE_LIMITED: number;
93
- INGRESS_FLAG_HTTPS: number;
94
- INGRESS_FLAG_TRUSTED_PROXY: number;
95
- INGRESS_FLAG_BODY_TRUNCATED: number;
96
- INGRESS_HV_JSON: number;
97
- INGRESS_HV_CORS_SIMPLE: number;
98
- INGRESS_HV_CORS_PREFLIGHT: number;
99
- INGRESS_HV_RATE_ACTIVE: number;
100
- INGRESS_HV_RATE_LIMITED: number;
101
- INGRESS_HV_COUNT: number;
102
- INGRESS_ERR_NONE: number;
103
- INGRESS_ERR_CORS_PREFLIGHT: number;
104
- INGRESS_ERR_RATE_LIMITED: number;
105
- INGRESS_ERR_BODY_TOO_LARGE: number;
106
- INGRESS_ERR_INVALID_JSON: number;
107
- INGRESS_ERR_SCHEMA_VALIDATION: number;
108
- INGRESS_ERR_BAD_REQUEST: number;
109
- INGRESS_ERR_REQUEST_TOO_LARGE: number;
110
- INGRESS_ERR_INTERNAL: number;
111
-
112
- crc32(input: Uint8Array): number;
113
- fnv1a64(input: Uint8Array): bigint;
114
-
115
- hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array;
116
- hmacSha256Verify(key: Uint8Array, data: Uint8Array, sig: Uint8Array): boolean;
117
-
118
- jsonValid(input: Uint8Array): boolean;
119
- jsonSumIds(input: Uint8Array): bigint;
120
-
121
- jsonPatch(doc: Uint8Array, patch: Uint8Array): Uint8Array;
122
-
123
- mimeFromExtension(ext: Uint8Array): Uint8Array;
124
- randomToken(byteLen: number): Uint8Array;
125
-
126
- urlEncode(input: Uint8Array): Uint8Array;
127
- urlDecode(input: Uint8Array): Uint8Array;
128
- urlDecodeBytes(input: Uint8Array): Uint8Array;
129
-
130
- validateEmail(input: Uint8Array): boolean;
131
- validateUuid(input: Uint8Array): boolean;
132
- validateIpv4(input: Uint8Array): boolean;
133
- validateIpv6(input: Uint8Array): boolean;
134
-
135
- wsAcceptKey(key: Uint8Array): Uint8Array;
136
-
137
- SchemaValidator: new (schema: Uint8Array) => SchemaValidatorInstance;
138
-
139
- initThreadPool(rayonThreads?: number): void;
140
- rayonNumThreads(): number;
141
-
142
- jsonValidBatchPacked(input: Uint8Array): Uint8Array;
143
- validateEmailBatchPacked(input: Uint8Array): Uint8Array;
144
- validateUuidBatchPacked(input: Uint8Array): Uint8Array;
145
- validateIpv4BatchPacked(input: Uint8Array): Uint8Array;
146
- validateIpv6BatchPacked(input: Uint8Array): Uint8Array;
147
-
148
- jsonSumBatchPacked(input: Uint8Array): Uint8Array;
149
- queryParseBatchPacked(input: Uint8Array): Uint8Array;
150
- cookieParseBatchPacked(input: Uint8Array): Uint8Array;
151
- httpParseRequestBatchPacked(input: Uint8Array): Uint8Array;
152
-
153
- httpParseRequestPacked(input: Uint8Array): Uint8Array;
154
-
155
- queryParsePacked(input: Uint8Array): Uint8Array;
156
-
157
- cookieParsePacked(input: Uint8Array): Uint8Array;
158
-
159
- crc32BatchPacked(input: Uint8Array): Uint8Array;
160
-
161
- // ── Batch metadata / counts ──
162
- jsonValidBatchCountPacked(input: Uint8Array): number;
163
- validateEmailBatchCountPacked(input: Uint8Array): number;
164
- validateUuidBatchCountPacked(input: Uint8Array): number;
165
- validateIpv4BatchCountPacked(input: Uint8Array): number;
166
- validateIpv6BatchCountPacked(input: Uint8Array): number;
167
- jsonSumBatchTotalPacked(input: Uint8Array): number;
168
- queryParseBatchTotalLenPacked(input: Uint8Array): number;
169
- cookieParseBatchTotalLenPacked(input: Uint8Array): number;
170
- httpParseRequestBatchTotalLenPacked(input: Uint8Array): number;
171
- }
172
-
173
- const addonPath = resolveAddonPath();
174
-
175
- let addon: NativeAddon;
176
- try {
177
- addon = require(addonPath);
178
- } catch (err) {
179
- const cause = err instanceof Error ? `\n Underlying cause: ${err.message}` : "";
180
- throw new Error(
181
- `Failed to load castrum native addon from:\n ${addonPath}\n` +
182
- `The addon exists but could not be loaded (ABI mismatch, missing system ` +
183
- `libraries, or a corrupt/partial artifact).\n` +
184
- `Run: bun run build\n` +
185
- `If the problem persists, verify the binary was built for this platform/CPU.` +
186
- cause,
187
- );
188
- }
189
-
190
- if (process.env.CASTRUM_DEBUG || process.env.RUST_BENCH_DEBUG) {
191
- console.log("Native addon loaded from:", addonPath);
192
- console.log("Exported keys:", Object.keys(addon).sort());
193
- }
194
-
195
- export interface SchemaValidatorInstance {
196
- validateBatchPackedCount(packed: Uint8Array): number;
197
- validateBatchPackedBitset(packed: Uint8Array): Uint8Array;
198
- validateBatchStreaming(batchBytes: Uint8Array): number;
199
- }
200
-
201
- export interface HmacSignerInstance {
202
- sign(data: Uint8Array): Uint8Array;
203
- verify(data: Uint8Array, sig: Uint8Array): boolean;
204
- }
205
-
206
- export default addon;
207
- export { addonPath };
1
+ // src/native/index.ts — Native addon layer (barrel).
2
+ //
3
+ // Re-exports the addon type surface (./types.ts) and the lazy loader
4
+ // (./loader.ts). Importing this barrel does NOT dlopen the addon — use
5
+ // `getAddon()` / `lazyAddon` at call time.
6
+
7
+ export * from './types'
8
+ export { getAddonPath, getAddon, lazyAddon } from './loader'
@@ -0,0 +1,240 @@
1
+ // src/native/loader.ts — Native addon path resolution + lazy loading.
2
+ //
3
+ // Importing this module does NOT dlopen the addon; `getAddon()` /
4
+ // `lazyAddon()` trigger loading exactly once on first use. This keeps
5
+ // `import castrum` as cheap as possible on Bun.
6
+
7
+ import { existsSync, readFileSync, statSync } from 'node:fs'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { join, dirname } from 'node:path'
10
+ import { createRequire } from 'node:module'
11
+ import { resolveEnvVar } from '../shared/env'
12
+ import type { NativeAddon } from './types'
13
+
14
+ const require = createRequire(import.meta.url)
15
+
16
+ /**
17
+ * x86-64-v3 feature flags (the exact set `scripts/build-v3.sh` compiles with).
18
+ * The v3 variant is only preferred when the host CPU exposes ALL of them —
19
+ * checking only `avx2` would let a partial-SSE4.2 machine dlopen a binary it
20
+ * cannot run (SIGILL is not catchable from JS, so we must be conservative).
21
+ */
22
+ const X8664_V3_FLAGS = ['avx2', 'bmi2', 'fma', 'sse4_2']
23
+
24
+ /**
25
+ * Detect x86-64-v3 support from `cpuinfo` text (Linux `/proc/cpuinfo` flags).
26
+ *
27
+ * Pure and unit-testable: the caller supplies the cpuinfo text (the real path
28
+ * reads `/proc/cpuinfo` on linux/x64). Returns false for any non-linux/x64
29
+ * platform and for any missing flag — a v3 binary dlopened on an unsupported
30
+ * CPU would SIGILL the whole Bun/Node process (not catchable from JS), so the
31
+ * default must be "no".
32
+ *
33
+ * @param cpuinfo the `flags:` line content, or `undefined` to read
34
+ * `/proc/cpuinfo` (only meaningful on linux/x64)
35
+ * @returns true when all x86-64-v3 feature flags are present
36
+ */
37
+ export function supportsX8664V3(cpuinfo?: string): boolean {
38
+ const text =
39
+ cpuinfo ??
40
+ (() => {
41
+ try {
42
+ return readFileSync('/proc/cpuinfo', 'utf8')
43
+ } catch {
44
+ return ''
45
+ }
46
+ })()
47
+ return X8664_V3_FLAGS.every((flag) => new RegExp(`\\b${flag}\\b`).test(text))
48
+ }
49
+
50
+ /**
51
+ * Resolve the addon path given EXPLICIT inputs (pure — unit-testable).
52
+ *
53
+ * `resolveAddonPath()` is a thin wrapper that reads `process.env` and the
54
+ * module location; this function contains all the resolution logic so tests
55
+ * can exercise env-override / multi-root / missing-addon behavior without
56
+ * mutating the process-global cache.
57
+ *
58
+ * Dual-binary CPU-detect: on linux/x64 when the host supports x86-64-v3, the
59
+ * `castrum.linux-x64-v3-gnu.node` variant is preferred (SIMD in crc32fast /
60
+ * sonic-rs / memchr / xxh3 / simdutf8 / mimalloc) and the first-existing
61
+ * candidate wins — so an absent v3 file falls through to the baseline names.
62
+ *
63
+ * @param envOverride an explicit `CASTRUM_NATIVE_LIBRARY_PATH`-style path, or
64
+ * `undefined` for none (the real call passes the env-var value)
65
+ * @param baseDir the directory of the current module (`dist/` or `src/native`)
66
+ * @param platform `process.platform`
67
+ * @param arch `process.arch`
68
+ * @param cpuinfo optional `/proc/cpuinfo` text (tests inject it; the real call
69
+ * omits it and reads the file lazily on linux/x64 only)
70
+ * @returns the first existing candidate path (throws if none exists)
71
+ */
72
+ export function resolveAddonPathFrom(
73
+ envOverride: string | undefined,
74
+ baseDir: string,
75
+ platform: string,
76
+ arch: string,
77
+ cpuinfo?: string,
78
+ ): string {
79
+ // napi-rs artifact naming: castrum.<platform>-<arch>[-<libc>].node
80
+ // e.g. linux-x64-gnu, linux-x64-musl, win32-x64-msvc, darwin-arm64.
81
+ const libcVariants =
82
+ platform === 'win32' ? ['msvc', 'gnu'] : platform === 'linux' ? ['gnu', 'musl'] : ['']
83
+
84
+ const names = libcVariants.map(
85
+ (libc) => `castrum.${platform}-${arch}${libc ? `-${libc}` : ''}.node`,
86
+ )
87
+ names.push('castrum.node')
88
+
89
+ // On a v3-capable linux/x64 host, prefer the SIMD variant first. The v3 file
90
+ // only ships in glibc (gnu) packages, so on musl the candidate is simply
91
+ // absent and resolution falls through to the baseline musl artifact.
92
+ if (platform === 'linux' && arch === 'x64' && supportsX8664V3(cpuinfo)) {
93
+ names.unshift('castrum.linux-x64-v3-gnu.node')
94
+ }
95
+
96
+ const candidates: string[] = []
97
+
98
+ // Explicit override (napi-rs convention + castrum/legacy aliases). May be a
99
+ // direct file path or a directory that contains the .node artifact.
100
+ if (envOverride) {
101
+ // A DIRECT file override must resolve to a FILE (a .node/loadable path).
102
+ // If the override is a DIRECTORY, only the joined `dir/<name>.node`
103
+ // candidates are valid — pushing the directory itself used to make
104
+ // `require(<dir>)` fail with MODULE_NOT_FOUND (no package.json/index inside).
105
+ const isFile = (p: string): boolean => {
106
+ try {
107
+ return statSync(p).isFile()
108
+ } catch {
109
+ return false
110
+ }
111
+ }
112
+ const overrideCandidates: string[] = []
113
+ if (existsSync(envOverride) && isFile(envOverride)) {
114
+ candidates.push(envOverride)
115
+ overrideCandidates.push(envOverride)
116
+ }
117
+ for (const name of names) {
118
+ const candidate = join(envOverride, name)
119
+ candidates.push(candidate)
120
+ overrideCandidates.push(candidate)
121
+ }
122
+ // A SET-but-misconfigured override must not silently fall through to the
123
+ // package roots — that would mask a typo'd path (a stale/root addon could
124
+ // load while the user believes their override is active). Throw a clear
125
+ // error instead, pointing at the expected override locations.
126
+ if (!overrideCandidates.some((c) => existsSync(c))) {
127
+ throw new Error(
128
+ `Could not find castrum native addon.\n` +
129
+ `CASTRUM_NATIVE_LIBRARY_PATH (or NAPI_RS_NATIVE_LIBRARY_PATH) is set to ` +
130
+ `"${envOverride}" but no loadable addon was found there.\n` +
131
+ `Expected one of:\n${overrideCandidates.map((c) => ` - ${c}`).join('\n')}\n` +
132
+ `Fix the path, or unset the variable to fall back to the package layout.`,
133
+ )
134
+ }
135
+ }
136
+
137
+ // Candidate roots cover BOTH layouts:
138
+ // - source layout (Bun, raw TS): <pkg>/src/native → <pkg> via ".."/"../.."
139
+ // - bundled layout (Node, dist/): <pkg>/dist → <pkg> via ".."
140
+ const roots = [
141
+ baseDir, // <pkg>/dist (bundled) or <pkg>/src/native
142
+ join(baseDir, '..'), // <pkg> when bundled into <pkg>/dist
143
+ join(baseDir, '..', '..'), // <pkg> when running from <pkg>/src/native
144
+ ]
145
+
146
+ for (const root of roots) {
147
+ for (const name of names) {
148
+ candidates.push(join(root, name))
149
+ }
150
+ }
151
+
152
+ // Local dev fallback: <pkg>/target/release/<name>.node (cargo-built artifact
153
+ // not yet copied to the package root). Note the artifact NAME must be
154
+ // appended — a bare directory is not loadable (regression guarded in tests).
155
+ for (const name of names) {
156
+ candidates.push(join(baseDir, '..', '..', 'target', 'release', name))
157
+ }
158
+
159
+ for (const candidate of candidates) {
160
+ if (existsSync(candidate)) {
161
+ return candidate
162
+ }
163
+ }
164
+
165
+ throw new Error(
166
+ `Could not find castrum native addon.\n` +
167
+ `Run: bun run build\n` +
168
+ `Looked in:\n${candidates.map((c) => ` - ${c}`).join('\n')}`,
169
+ )
170
+ }
171
+
172
+ function resolveAddonPath(): string {
173
+ return resolveAddonPathFrom(
174
+ resolveEnvVar('CASTRUM_NATIVE_LIBRARY_PATH', ['NAPI_RS_NATIVE_LIBRARY_PATH']),
175
+ dirname(fileURLToPath(import.meta.url)),
176
+ process.platform,
177
+ process.arch,
178
+ )
179
+ }
180
+
181
+ let cachedPath: string | undefined
182
+
183
+ /** Resolve the addon file path exactly once (lazy). */
184
+ export function getAddonPath(): string {
185
+ cachedPath ??= resolveAddonPath()
186
+ return cachedPath
187
+ }
188
+
189
+ let cachedAddon: NativeAddon | undefined
190
+
191
+ /**
192
+ * Load the native addon exactly once, lazily.
193
+ *
194
+ * The first call triggers loading (see {@link getAddonPath} for resolution).
195
+ */
196
+ export function getAddon(): NativeAddon {
197
+ if (cachedAddon === undefined) {
198
+ const addonPath = getAddonPath()
199
+ let loaded: NativeAddon
200
+ try {
201
+ loaded = require(addonPath)
202
+ } catch (err) {
203
+ const cause = err instanceof Error ? `\n Underlying cause: ${err.message}` : ''
204
+ throw new Error(
205
+ `Failed to load castrum native addon from:\n ${addonPath}\n` +
206
+ `The addon exists but could not be loaded (ABI mismatch, missing system ` +
207
+ `libraries, or a corrupt/partial artifact).\n` +
208
+ `Run: bun run build\n` +
209
+ `If the problem persists, verify the binary was built for this platform/CPU.` +
210
+ cause,
211
+ )
212
+ }
213
+
214
+ if (resolveEnvVar('CASTRUM_DEBUG') !== undefined) {
215
+ console.log('Native addon loaded from:', addonPath)
216
+ console.log('Exported keys:', Object.keys(loaded).sort())
217
+ }
218
+
219
+ cachedAddon = loaded
220
+ }
221
+ return cachedAddon
222
+ }
223
+
224
+ /**
225
+ * A lazily-loading proxy over the native addon.
226
+ *
227
+ * Use this in modules that only touch the addon at call time (e.g. the FFI
228
+ * wrappers) so that importing them does not dlopen the addon until the first
229
+ * real native call. All existing `addon.X` / `new addon.Y(...)` call sites
230
+ * keep working unchanged.
231
+ */
232
+ export function lazyAddon<T extends object>(loader: () => T): T {
233
+ let loaded: T | undefined
234
+ return new Proxy({} as T, {
235
+ get(_target, prop: string | symbol) {
236
+ loaded ??= loader()
237
+ return Reflect.get(loaded, prop)
238
+ },
239
+ })
240
+ }