castrum 0.9.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.
- package/README.md +153 -630
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1374 -1772
- package/dist/index.js +5570 -4169
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +106 -14
- package/docs/BENCHMARKS.md +83 -47
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +8 -6
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +15 -32
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +5 -4
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +46 -26
- package/docs/adr/0003-bun-builtins-delegation.md +6 -5
- package/docs/bun-builtins-decision-matrix.md +25 -18
- package/index.ts +9 -16
- package/package.json +36 -24
- package/src/ingress/body.ts +167 -27
- package/src/ingress/handlers.ts +91 -15
- package/src/ingress/health.ts +1 -0
- package/src/ingress/index.ts +39 -27
- package/src/ingress/packing/gather-raw-headers.ts +85 -21
- package/src/ingress/packing/header-packing.ts +5 -5
- package/src/ingress/packing/input-packer.ts +17 -3
- package/src/ingress/packing/select-headers.ts +12 -6
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +42 -1
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +3 -13
- package/src/ingress/routes/index.ts +5 -4
- package/src/ingress/routes/json-write.ts +7 -31
- package/src/ingress/routes/options.ts +2 -12
- package/src/ingress/routes/read.ts +3 -13
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +6 -6
- package/src/ingress/server.ts +45 -13
- package/src/ingress/types.ts +35 -1
- package/src/integration/pipeline.ts +3 -0
- package/src/integration/streaming.ts +2 -3
- package/src/integration/websocket.ts +4 -3
- package/src/loader/batch.ts +4 -0
- package/src/loader/cost.ts +1 -0
- package/src/loader/index.ts +43 -228
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1063 -829
- package/src/native/loader.ts +52 -6
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +41 -238
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +1 -1
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +5 -6
- package/src/rust-ffi/context.ts +39 -10
- package/src/rust-ffi/index.ts +28 -17
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +1 -1
- package/src/rust-ffi/packed.ts +2 -2
- package/src/rust-ffi/proven.ts +16 -30
- package/src/rust-ffi/scalar/crypto.ts +188 -107
- package/src/rust-ffi/scalar/factories.ts +253 -27
- package/src/rust-ffi/scalar/hashing.ts +43 -41
- package/src/rust-ffi/scalar/http.ts +208 -77
- package/src/rust-ffi/scalar/index.ts +3 -3
- package/src/rust-ffi/scalar/interface.ts +88 -213
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +28 -12
- package/src/rust-ffi/scalar/payload.ts +159 -42
- package/src/rust-ffi/text.ts +31 -5
- package/src/selection.ts +27 -31
- package/src/shared/adaptive.ts +1 -2
- package/src/shared/buffer-pool.ts +72 -17
- package/src/shared/bytes.ts +75 -6
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +4 -4
- package/src/shared/log.ts +1 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/{packed.ts → packed/wire.ts} +31 -39
- package/src/shared/proven.ts +189 -496
- package/src/shared/runtime.ts +20 -25
- package/src/shared/uuid.ts +12 -28
- package/src/data/json-rows.ts +0 -33
- package/src/rust-ffi/batch.ts +0 -431
- package/src/shared/bench-classify.ts +0 -209
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/runtime/uuid.ts — UUIDv7 generation (Bun built-in, Node fallback).
|
|
2
|
+
//
|
|
3
|
+
// Deliberately does NOT implement UUIDv7 in Rust: the decision matrix measured
|
|
4
|
+
// `Bun.randomUUIDv7` ~2x faster than the FFI-crossing rust random-token path,
|
|
5
|
+
// so this delegates to the Bun built-in and falls back to
|
|
6
|
+
// `crypto.randomUUID` (RFC 4122 v4) under Node. Formerly in
|
|
7
|
+
// `src/shared/uuid.ts`; the branches now live here, selected once at load.
|
|
8
|
+
|
|
9
|
+
import { detectedRuntime } from './detect'
|
|
10
|
+
import type { UuidAdapter } from './types'
|
|
11
|
+
|
|
12
|
+
/** Last-resort synchronous UUIDv4 (only if `crypto.randomUUID` is unavailable). */
|
|
13
|
+
function fallbackRandomUuid(): string {
|
|
14
|
+
const bytes = new Uint8Array(16)
|
|
15
|
+
const c = globalThis.crypto
|
|
16
|
+
if (c?.getRandomValues) {
|
|
17
|
+
c.getRandomValues(bytes)
|
|
18
|
+
} else {
|
|
19
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256)
|
|
20
|
+
}
|
|
21
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40 // version 4
|
|
22
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80 // variant 10xx
|
|
23
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
24
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function bunUuidv7(): string {
|
|
28
|
+
return (
|
|
29
|
+
(globalThis as { Bun?: { randomUUIDv7?: () => string } }).Bun?.randomUUIDv7?.() ??
|
|
30
|
+
globalThis.crypto?.randomUUID?.() ??
|
|
31
|
+
fallbackRandomUuid()
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nodeUuidv7(): string {
|
|
36
|
+
return globalThis.crypto?.randomUUID?.() ?? fallbackRandomUuid()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build the uuid adapter for a runtime. */
|
|
40
|
+
export function createUuidAdapter(name: 'bun' | 'node' | 'unknown'): UuidAdapter {
|
|
41
|
+
return name === 'bun' ? { uuidv7: bunUuidv7 } : { uuidv7: nodeUuidv7 }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The shared uuid adapter for the detected runtime (selected once at load). */
|
|
45
|
+
export const runtimeUuid: UuidAdapter = createUuidAdapter(detectedRuntime)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/runtime/websocket.ts — platform WebSocket upgrade adapter.
|
|
2
|
+
//
|
|
3
|
+
// `createWebSocketUpgrade` returns the RFC 6455 101 handshake on Bun (whose
|
|
4
|
+
// `Response` can carry status 101) and throws a clear Bun-only error on Node
|
|
5
|
+
// (whose fetch `Response` rejects 101 — Node users must use the server-node
|
|
6
|
+
// `upgrade` option instead). The handshake computation lives in
|
|
7
|
+
// `src/integration/websocket.ts`; this adapter owns the runtime branch.
|
|
8
|
+
|
|
9
|
+
import type { WebSocketUpgradeOptions, WebSocketUpgradeResult } from '../integration/websocket'
|
|
10
|
+
import { createWebSocketUpgrade as buildBunUpgrade } from '../integration/websocket'
|
|
11
|
+
import { detectedRuntime } from './detect'
|
|
12
|
+
import type { WebsocketAdapter } from './types'
|
|
13
|
+
|
|
14
|
+
const BUN_ONLY_ERROR =
|
|
15
|
+
"createWebSocketUpgrade is Bun-only: Node's Response cannot carry status 101. " +
|
|
16
|
+
"On Node, use createIngressServerNode's `upgrade` option and compute the " +
|
|
17
|
+
'accept key with rust.text.wsAcceptKey().'
|
|
18
|
+
|
|
19
|
+
/** The shared websocket adapter for the detected host runtime. */
|
|
20
|
+
export const runtimeWebsocket: WebsocketAdapter = {
|
|
21
|
+
createWebSocketUpgrade(
|
|
22
|
+
req: Request,
|
|
23
|
+
opts?: WebSocketUpgradeOptions,
|
|
24
|
+
): WebSocketUpgradeResult | null {
|
|
25
|
+
if (detectedRuntime !== 'bun') {
|
|
26
|
+
throw new Error(BUN_ONLY_ERROR)
|
|
27
|
+
}
|
|
28
|
+
return buildBunUpgrade(req, opts)
|
|
29
|
+
},
|
|
30
|
+
}
|
package/src/rust-ffi/addon.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// module does NOT dlopen the addon — the first native call triggers loading
|
|
5
5
|
// exactly once.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { getAddon, lazyAddon, type NativeAddon } from '../native'
|
|
8
8
|
|
|
9
9
|
/** The shared lazy addon proxy (typed as the full native surface). */
|
|
10
10
|
export const addon: NativeAddon = lazyAddon(getAddon)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/rust-ffi/batch/build.ts — `buildBatch` implementation.
|
|
2
|
+
//
|
|
3
|
+
// Packs an array of byte slices into the packed wire format, calls the native
|
|
4
|
+
// batch function, and unpacks the result into the friendly JS shape. Each
|
|
5
|
+
// method wires one `RustBatch` (./types.ts) member to its `*BatchPacked` napi
|
|
6
|
+
// fn via the shared pool-native resolver (context.ts).
|
|
7
|
+
//
|
|
8
|
+
// The `via` / `via2` builders below collapse the pack → native → unpack
|
|
9
|
+
// plumbing every bulk op shares; each public method keeps its exact signature,
|
|
10
|
+
// so the table stays type-safe against `RustBatch`.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
schemaValidateBatch,
|
|
14
|
+
schemaValidateBatchCount,
|
|
15
|
+
unpackBitset,
|
|
16
|
+
unpackByteResults,
|
|
17
|
+
unpackI64ArrayAsBigInt,
|
|
18
|
+
unpackMultipartParts,
|
|
19
|
+
unpackU32Array,
|
|
20
|
+
unpackU64ArrayAsBigInt,
|
|
21
|
+
withPackScratch,
|
|
22
|
+
withPackScratch2,
|
|
23
|
+
} from '../../shared/packed'
|
|
24
|
+
import { type RustClientContext, resolvePoolNative } from '../context'
|
|
25
|
+
import type { RustBatch } from './types'
|
|
26
|
+
|
|
27
|
+
// Native batch fns are process-wide singletons (one lazy addon). `resolvePoolNative`
|
|
28
|
+
// resolves each once (triggering pool init + addon load on first use) and caches
|
|
29
|
+
// it, so the hot path skips the lazy-addon Proxy get on every call — the same
|
|
30
|
+
// cache `resolveNative`/packed use (context.ts), with pool init for the
|
|
31
|
+
// rayon-backed batch surface.
|
|
32
|
+
function nativeFn(ctx: RustClientContext, name: string): (...args: unknown[]) => unknown {
|
|
33
|
+
return resolvePoolNative(ctx, name)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a single-list bulk op: `withPackScratch(items)` →
|
|
38
|
+
* `unpack(native(packed, ...extras))`. `R` is inferred from `unpack`, so the
|
|
39
|
+
* result stays assignable to the matching `RustBatch` method.
|
|
40
|
+
*/
|
|
41
|
+
function via<R>(
|
|
42
|
+
ctx: RustClientContext,
|
|
43
|
+
name: string,
|
|
44
|
+
unpack: (bytes: Uint8Array) => R,
|
|
45
|
+
extras: unknown[] = [],
|
|
46
|
+
): (items: Uint8Array[]) => R {
|
|
47
|
+
return (items) =>
|
|
48
|
+
withPackScratch(items, (packed) => unpack(nativeFn(ctx, name)(packed, ...extras) as Buffer))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a two-list bulk op (docs+patches, items+sigs, ...): `withPackScratch2(a, b)`
|
|
53
|
+
* → `unpack(native(packedA, packedB, ...extras))`.
|
|
54
|
+
*/
|
|
55
|
+
function via2<R>(
|
|
56
|
+
ctx: RustClientContext,
|
|
57
|
+
name: string,
|
|
58
|
+
unpack: (bytes: Uint8Array) => R,
|
|
59
|
+
extras: unknown[] = [],
|
|
60
|
+
): (a: Uint8Array[], b: Uint8Array[]) => R {
|
|
61
|
+
return (a, b) =>
|
|
62
|
+
withPackScratch2(a, b, (packedA, packedB) =>
|
|
63
|
+
unpack(nativeFn(ctx, name)(packedA, packedB, ...extras) as Buffer),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Build the `batch` namespace for a client context. */
|
|
68
|
+
export function buildBatch(ctx: RustClientContext): RustBatch {
|
|
69
|
+
return {
|
|
70
|
+
// ── Single-list, no extras ──
|
|
71
|
+
jsonValid: via(ctx, 'jsonValidBatchPacked', unpackBitset),
|
|
72
|
+
crc32: via(ctx, 'crc32BatchPacked', unpackU32Array),
|
|
73
|
+
validateEmail: via(ctx, 'validateEmailBatchPacked', unpackBitset),
|
|
74
|
+
validateUuid: via(ctx, 'validateUuidBatchPacked', unpackBitset),
|
|
75
|
+
validateIpv4: via(ctx, 'validateIpv4BatchPacked', unpackBitset),
|
|
76
|
+
validateIpv6: via(ctx, 'validateIpv6BatchPacked', unpackBitset),
|
|
77
|
+
jsonSumIds: via(ctx, 'jsonSumBatchPacked', unpackI64ArrayAsBigInt),
|
|
78
|
+
queryParse: via(ctx, 'queryParseBatchPacked', unpackByteResults),
|
|
79
|
+
cookieParse: via(ctx, 'cookieParseBatchPacked', unpackByteResults),
|
|
80
|
+
formParse: via(ctx, 'formParseBatchPacked', unpackByteResults),
|
|
81
|
+
signCookie: (items, secret) =>
|
|
82
|
+
via(ctx, 'signCookieBatchPacked', unpackByteResults, [secret])(items),
|
|
83
|
+
verifyCookie: (items, secret) =>
|
|
84
|
+
via(ctx, 'verifyCookieBatchPacked', unpackBitset, [secret])(items),
|
|
85
|
+
csrfVerify: (items, secret) =>
|
|
86
|
+
via(ctx, 'csrfVerifyBatchPacked', unpackBitset, [secret])(items),
|
|
87
|
+
httpParseRequest: via(ctx, 'httpParseRequestBatchPacked', unpackByteResults),
|
|
88
|
+
fnv1a64: via(ctx, 'fnv1A64BatchPacked', unpackU64ArrayAsBigInt),
|
|
89
|
+
etag: (items, weak) => via(ctx, 'etagBatchPacked', unpackByteResults, [weak ?? null])(items),
|
|
90
|
+
urlEncode: via(ctx, 'urlEncodeBatchPacked', unpackByteResults),
|
|
91
|
+
urlDecode: via(ctx, 'urlDecodeBatchPacked', unpackByteResults),
|
|
92
|
+
urlDecodeBytes: via(ctx, 'urlDecodeBytesBatchPacked', unpackByteResults),
|
|
93
|
+
mimeFromExtension: via(ctx, 'mimeFromExtensionBatchPacked', unpackByteResults),
|
|
94
|
+
wsAcceptKey: via(ctx, 'wsAcceptKeyBatchPacked', unpackByteResults),
|
|
95
|
+
base64UrlEncode: (items) =>
|
|
96
|
+
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [true, false])(items),
|
|
97
|
+
base64UrlDecode: (items) =>
|
|
98
|
+
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [true, false])(items),
|
|
99
|
+
jsonPatch: (docs, patches) => via2(ctx, 'jsonPatchBatchPacked', unpackByteResults)(docs, patches),
|
|
100
|
+
hexEncode: via(ctx, 'hexEncodeBatchPacked', unpackByteResults),
|
|
101
|
+
hexDecode: via(ctx, 'hexDecodeBatchPacked', unpackByteResults),
|
|
102
|
+
base64Encode: (items, urlSafe, padding) =>
|
|
103
|
+
via(ctx, 'base64EncodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(items),
|
|
104
|
+
base64Decode: (items, urlSafe, padding) =>
|
|
105
|
+
via(ctx, 'base64DecodeBatchPacked', unpackByteResults, [urlSafe ?? null, padding ?? null])(items),
|
|
106
|
+
schemaValidate(validator, items) {
|
|
107
|
+
return schemaValidateBatch(validator, items)
|
|
108
|
+
},
|
|
109
|
+
schemaValidateCount(validator, items) {
|
|
110
|
+
return schemaValidateBatchCount(validator, items)
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// ── Backend-framework features ──
|
|
114
|
+
passwordHash: (passwords, salt, options) =>
|
|
115
|
+
via(ctx, 'passwordHashBatchPacked', unpackByteResults, [salt, options ?? null])(passwords),
|
|
116
|
+
aeadEncrypt: (key, nonce, items, algorithm) =>
|
|
117
|
+
via(ctx, 'aeadEncryptBatchPacked', unpackByteResults, [key, nonce, algorithm ?? null])(items),
|
|
118
|
+
aeadDecrypt: (key, nonce, items, algorithm) =>
|
|
119
|
+
via(ctx, 'aeadDecryptBatchPacked', unpackByteResults, [key, nonce, algorithm ?? null])(items),
|
|
120
|
+
gzipCompress: (items, level) =>
|
|
121
|
+
via(ctx, 'gzipCompressBatchPacked', unpackByteResults, [level ?? null])(items),
|
|
122
|
+
gzipDecompress: (items, maxDecompressed) =>
|
|
123
|
+
via(ctx, 'gzipDecompressBatchPacked', unpackByteResults, [maxDecompressed ?? null])(items),
|
|
124
|
+
brotliCompress: (items, quality) =>
|
|
125
|
+
via(ctx, 'brotliCompressBatchPacked', unpackByteResults, [quality ?? null])(items),
|
|
126
|
+
brotliDecompress: (items, maxDecompressed) =>
|
|
127
|
+
via(ctx, 'brotliDecompressBatchPacked', unpackByteResults, [maxDecompressed ?? null])(items),
|
|
128
|
+
jwtVerify: (tokens, secret, nowSeconds) =>
|
|
129
|
+
via(ctx, 'jwtVerifyBatchPacked', unpackBitset, [secret, nowSeconds])(tokens),
|
|
130
|
+
hmacSha256: (items, key) => via(ctx, 'hmacSha256BatchPacked', unpackByteResults, [key])(items),
|
|
131
|
+
hmacSha256Verify: (items, sigs, key) =>
|
|
132
|
+
via2(ctx, 'hmacSha256VerifyBatchPacked', unpackBitset, [key])(items, sigs),
|
|
133
|
+
// ── Two-list ──
|
|
134
|
+
passwordVerify: (items, phcs) => via2(ctx, 'passwordVerifyBatchPacked', unpackBitset)(items, phcs),
|
|
135
|
+
urlResolve: (bases, references) =>
|
|
136
|
+
via2(ctx, 'urlResolveBatchPacked', unpackByteResults)(bases, references),
|
|
137
|
+
templateRender: (source, contexts) =>
|
|
138
|
+
via(ctx, 'templateRenderBatchPacked', unpackByteResults, [source])(contexts),
|
|
139
|
+
jwtSign: (items, secret, ttlSeconds, nowSeconds) =>
|
|
140
|
+
via(ctx, 'jwtSignBatchPacked', unpackByteResults, [
|
|
141
|
+
secret,
|
|
142
|
+
ttlSeconds ?? null,
|
|
143
|
+
nowSeconds ?? Math.floor(Date.now() / 1000),
|
|
144
|
+
])(items),
|
|
145
|
+
sseEncode: (items, event, id, retry) =>
|
|
146
|
+
via(ctx, 'sseEncodeBatchPacked', unpackByteResults, [event ?? null, id ?? null, retry ?? null])(
|
|
147
|
+
items,
|
|
148
|
+
),
|
|
149
|
+
wsFrameEncode: (items, opcode, mask, fin) =>
|
|
150
|
+
via(ctx, 'wsFrameEncodeBatchPacked', unpackByteResults, [opcode, mask, fin])(items),
|
|
151
|
+
wsFrameDecode: via(ctx, 'wsFrameDecodeBatchPacked', unpackByteResults),
|
|
152
|
+
multipartParse: (items, boundary) =>
|
|
153
|
+
via(ctx, 'multipartParseBatchPacked', unpackMultipartParts, [boundary])(items),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// src/rust-ffi/batch/index.ts — Array-of-bytes batch namespace (barrel).
|
|
2
|
+
//
|
|
3
|
+
// Re-exports the `RustBatch` interface (./types.ts) and its builder
|
|
4
|
+
// (./build.ts) so existing `import { buildBatch, type RustBatch } from
|
|
5
|
+
// '../rust-ffi/batch'` call sites keep working.
|
|
6
|
+
|
|
7
|
+
export { buildBatch } from './build'
|
|
8
|
+
export type { RustBatch } from './types'
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/rust-ffi/batch/types.ts — Array-of-bytes batch namespace type.
|
|
2
|
+
//
|
|
3
|
+
// The `RustBatch` interface — the friendly JS shape of `rust.batch.*`
|
|
4
|
+
// (bitsets, u32 arrays, i64 arrays, byte results) — separated from the
|
|
5
|
+
// implementation (./build.ts) so type-only consumers don't pull the native
|
|
6
|
+
// resolver.
|
|
7
|
+
|
|
8
|
+
import type { MultipartPart, PasswordHashOptions } from '../../native'
|
|
9
|
+
import type { SchemaValidator } from '../../shared/packed'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The `rust.batch.*` array-of-bytes batch surface: N items in → one packed
|
|
13
|
+
* native call → bitsets / typed arrays / byte arrays out. Separated from the
|
|
14
|
+
* implementation (`./build.ts`) so type-only consumers don't pull the native
|
|
15
|
+
* resolver.
|
|
16
|
+
*/
|
|
17
|
+
export interface RustBatch {
|
|
18
|
+
jsonValid(items: Uint8Array[]): Uint8Array
|
|
19
|
+
crc32(items: Uint8Array[]): Uint32Array
|
|
20
|
+
validateEmail(items: Uint8Array[]): Uint8Array
|
|
21
|
+
validateUuid(items: Uint8Array[]): Uint8Array
|
|
22
|
+
validateIpv4(items: Uint8Array[]): Uint8Array
|
|
23
|
+
validateIpv6(items: Uint8Array[]): Uint8Array
|
|
24
|
+
jsonSumIds(items: Uint8Array[]): BigInt64Array
|
|
25
|
+
queryParse(items: Uint8Array[]): Uint8Array[]
|
|
26
|
+
cookieParse(items: Uint8Array[]): Uint8Array[]
|
|
27
|
+
httpParseRequest(items: Uint8Array[]): Uint8Array[]
|
|
28
|
+
jsonPatch(docs: Uint8Array[], patches: Uint8Array[]): Uint8Array[]
|
|
29
|
+
hexEncode(items: Uint8Array[]): Uint8Array[]
|
|
30
|
+
hexDecode(items: Uint8Array[]): Uint8Array[]
|
|
31
|
+
base64Encode(items: Uint8Array[], urlSafe?: boolean, padding?: boolean): Uint8Array[]
|
|
32
|
+
base64Decode(items: Uint8Array[], urlSafe?: boolean, padding?: boolean): Uint8Array[]
|
|
33
|
+
formParse(items: Uint8Array[]): Uint8Array[]
|
|
34
|
+
signCookie(items: Uint8Array[], secret: Uint8Array): Uint8Array[]
|
|
35
|
+
verifyCookie(items: Uint8Array[], secret: Uint8Array): Uint8Array
|
|
36
|
+
csrfVerify(items: Uint8Array[], secret: Uint8Array): Uint8Array
|
|
37
|
+
schemaValidate(validator: SchemaValidator, items: Uint8Array[]): Uint8Array
|
|
38
|
+
schemaValidateCount(validator: SchemaValidator, items: Uint8Array[]): number
|
|
39
|
+
fnv1a64(items: Uint8Array[]): BigUint64Array
|
|
40
|
+
etag(items: Uint8Array[], weak?: boolean | null): Uint8Array[]
|
|
41
|
+
urlEncode(items: Uint8Array[]): Uint8Array[]
|
|
42
|
+
urlDecode(items: Uint8Array[]): Uint8Array[]
|
|
43
|
+
urlDecodeBytes(items: Uint8Array[]): Uint8Array[]
|
|
44
|
+
mimeFromExtension(items: Uint8Array[]): Uint8Array[]
|
|
45
|
+
wsAcceptKey(items: Uint8Array[]): Uint8Array[]
|
|
46
|
+
base64UrlEncode(items: Uint8Array[]): Uint8Array[]
|
|
47
|
+
base64UrlDecode(items: Uint8Array[]): Uint8Array[]
|
|
48
|
+
|
|
49
|
+
// ── Backend-framework features ──
|
|
50
|
+
passwordHash(
|
|
51
|
+
passwords: Uint8Array[],
|
|
52
|
+
salt: Uint8Array,
|
|
53
|
+
options?: PasswordHashOptions | null,
|
|
54
|
+
): Uint8Array[]
|
|
55
|
+
aeadEncrypt(
|
|
56
|
+
key: Uint8Array,
|
|
57
|
+
nonce: Uint8Array,
|
|
58
|
+
items: Uint8Array[],
|
|
59
|
+
algorithm?: string | null,
|
|
60
|
+
): Uint8Array[]
|
|
61
|
+
aeadDecrypt(
|
|
62
|
+
key: Uint8Array,
|
|
63
|
+
nonce: Uint8Array,
|
|
64
|
+
items: Uint8Array[],
|
|
65
|
+
algorithm?: string | null,
|
|
66
|
+
): Uint8Array[]
|
|
67
|
+
gzipCompress(items: Uint8Array[], level?: number | null): Uint8Array[]
|
|
68
|
+
gzipDecompress(items: Uint8Array[], maxDecompressed?: number | null): Uint8Array[]
|
|
69
|
+
brotliCompress(items: Uint8Array[], quality?: number | null): Uint8Array[]
|
|
70
|
+
brotliDecompress(items: Uint8Array[], maxDecompressed?: number | null): Uint8Array[]
|
|
71
|
+
jwtVerify(tokens: Uint8Array[], secret: Uint8Array, nowSeconds: number): Uint8Array
|
|
72
|
+
hmacSha256(items: Uint8Array[], key: Uint8Array): Uint8Array[]
|
|
73
|
+
hmacSha256Verify(items: Uint8Array[], sigs: Uint8Array[], key: Uint8Array): Uint8Array
|
|
74
|
+
passwordVerify(items: Uint8Array[], phcs: Uint8Array[]): Uint8Array
|
|
75
|
+
urlResolve(bases: Uint8Array[], references: Uint8Array[]): Uint8Array[]
|
|
76
|
+
jwtSign(
|
|
77
|
+
items: Uint8Array[],
|
|
78
|
+
secret: Uint8Array,
|
|
79
|
+
ttlSeconds?: number | null,
|
|
80
|
+
nowSeconds?: number,
|
|
81
|
+
): Uint8Array[]
|
|
82
|
+
sseEncode(
|
|
83
|
+
items: Uint8Array[],
|
|
84
|
+
event?: string | null,
|
|
85
|
+
id?: string | null,
|
|
86
|
+
retry?: number | null,
|
|
87
|
+
): Uint8Array[]
|
|
88
|
+
wsFrameEncode(items: Uint8Array[], opcode: number, mask: boolean, fin: boolean): Uint8Array[]
|
|
89
|
+
wsFrameDecode(items: Uint8Array[]): Uint8Array[]
|
|
90
|
+
multipartParse(items: Uint8Array[], boundary: Uint8Array): MultipartPart[][]
|
|
91
|
+
templateRender(source: string, contexts: Uint8Array[]): Uint8Array[]
|
|
92
|
+
}
|
package/src/rust-ffi/client.ts
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
// Composes the per-namespace builders (scalar / text / batch / packed) over a
|
|
4
4
|
// shared per-instance context (./context.ts) and exposes `configure`.
|
|
5
5
|
|
|
6
|
-
import { createContext } from './context'
|
|
7
|
-
import { buildText, type RustText } from './text'
|
|
8
6
|
import { buildBatch, type RustBatch } from './batch'
|
|
7
|
+
import { createContext } from './context'
|
|
8
|
+
import type { RustOptions } from './options'
|
|
9
9
|
import { buildPacked, type RustPacked } from './packed'
|
|
10
10
|
import { buildScalar, type RustScalar } from './scalar'
|
|
11
|
-
import {
|
|
12
|
-
import type { RustOptions } from './options'
|
|
11
|
+
import { buildText, type RustText } from './text'
|
|
13
12
|
|
|
14
13
|
/** The full Rust FFI client (scalar methods + namespaces + configuration). */
|
|
15
14
|
export interface RustClient extends RustScalar {
|
|
@@ -56,10 +55,10 @@ export function createRust(options: RustOptions = {}): RustClient {
|
|
|
56
55
|
|
|
57
56
|
// ── Transport introspection (FFI-primary on Bun; napi is the fallback) ──
|
|
58
57
|
transport() {
|
|
59
|
-
return
|
|
58
|
+
return ctx.runtime.transport.name
|
|
60
59
|
},
|
|
61
60
|
ffiActive() {
|
|
62
|
-
return
|
|
61
|
+
return ctx.runtime.transport.ffiActive()
|
|
63
62
|
},
|
|
64
63
|
|
|
65
64
|
// ── Configuration ──
|
package/src/rust-ffi/context.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
// (initialized flag / pending thread count) stays encapsulated behind accessor
|
|
7
7
|
// functions so it cannot be corrupted externally.
|
|
8
8
|
|
|
9
|
+
import type { HmacSignerInstance } from '../native'
|
|
10
|
+
import type { BunFFI } from '../native/ffi'
|
|
11
|
+
import { runtimeNative } from '../runtime/native'
|
|
12
|
+
import type { NativeRuntimeAdapter, TransportAdapter } from '../runtime/types'
|
|
9
13
|
import { addon } from './addon'
|
|
10
|
-
import { resolveRayonThreads } from './options'
|
|
11
|
-
import { normalizeExt } from './options'
|
|
12
14
|
import type { RustOptions } from './options'
|
|
13
|
-
import
|
|
15
|
+
import { normalizeExt, resolveRayonThreads } from './options'
|
|
14
16
|
|
|
15
17
|
// Module-level cache of resolved native fns. The native addon is a process
|
|
16
18
|
// singleton, so the resolved function is identical across all client contexts;
|
|
@@ -20,9 +22,11 @@ import type { HmacSignerInstance } from '../native'
|
|
|
20
22
|
const nativeFnCache = new Map<string, unknown>()
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
* Resolve a native
|
|
24
|
-
*
|
|
25
|
-
*
|
|
25
|
+
* Resolve a native op on first use and cache it for all subsequent calls —
|
|
26
|
+
* FFI-FIRST via the runtime adapter's transport (`bun:ffi` on Bun, napi
|
|
27
|
+
* fallback otherwise), so builders no longer repeat `getBunFFI()` inline.
|
|
28
|
+
* Preserves the lazy-load contract (the addon is only loaded on the first
|
|
29
|
+
* real native call) while removing per-call Proxy overhead.
|
|
26
30
|
*/
|
|
27
31
|
export function resolveNative(
|
|
28
32
|
ctx: RustClientContext,
|
|
@@ -30,7 +34,8 @@ export function resolveNative(
|
|
|
30
34
|
): (...args: unknown[]) => unknown {
|
|
31
35
|
let f = nativeFnCache.get(name)
|
|
32
36
|
if (f === undefined) {
|
|
33
|
-
|
|
37
|
+
const fn = ctx.runtime.transport.resolve(name)
|
|
38
|
+
f = fn ?? (ctx.addon as unknown as Record<string, unknown>)[name]
|
|
34
39
|
nativeFnCache.set(name, f)
|
|
35
40
|
}
|
|
36
41
|
return f as (...args: unknown[]) => unknown
|
|
@@ -42,7 +47,8 @@ export function resolveNative(
|
|
|
42
47
|
* `resolveNative` (plain scalars) must NOT init the pool so
|
|
43
48
|
* `rust.configure({ rayonThreads })` can still take effect at startup.
|
|
44
49
|
* Shares the SAME `nativeFnCache` as `resolveNative` (the batch fn names are
|
|
45
|
-
* disjoint from the scalar names, so the shared cache is safe).
|
|
50
|
+
* disjoint from the scalar names, so the shared cache is safe). FFI-first via
|
|
51
|
+
* the runtime adapter's transport, like `resolveNative`.
|
|
46
52
|
*/
|
|
47
53
|
export function resolvePoolNative(
|
|
48
54
|
ctx: RustClientContext,
|
|
@@ -51,16 +57,38 @@ export function resolvePoolNative(
|
|
|
51
57
|
let f = nativeFnCache.get(name)
|
|
52
58
|
if (f === undefined) {
|
|
53
59
|
ctx.ensurePool()
|
|
54
|
-
|
|
60
|
+
const fn = ctx.runtime.transport.resolve(name)
|
|
61
|
+
f = fn ?? (ctx.addon as unknown as Record<string, unknown>)[name]
|
|
55
62
|
nativeFnCache.set(name, f)
|
|
56
63
|
}
|
|
57
64
|
return f as (...args: unknown[]) => unknown
|
|
58
65
|
}
|
|
59
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Lazy memoized `bun:ffi` resolver for the scalar builders.
|
|
69
|
+
*
|
|
70
|
+
* Returns a closure that resolves `transport.ffi` ONCE — on the first call —
|
|
71
|
+
* and reads the local from then on. This keeps the FFI bind DEFERRED
|
|
72
|
+
* (importing `rust` does not eagerly dlopen; the builder factories run at
|
|
73
|
+
* module load) while removing the per-call `() => transport.ffi` getter hop
|
|
74
|
+
* from the hot path. The transport's own cache already makes the steady-state
|
|
75
|
+
* resolve a single `undefined`-check; this collapses the getter + inner
|
|
76
|
+
* closure into one local read.
|
|
77
|
+
*/
|
|
78
|
+
export function memoizeFfi(transport: TransportAdapter): () => BunFFI | null {
|
|
79
|
+
let resolved: BunFFI | null | undefined
|
|
80
|
+
return () => {
|
|
81
|
+
if (resolved === undefined) resolved = transport.ffi
|
|
82
|
+
return resolved
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
60
86
|
/** Shared per-instance state passed to the namespace/scalar builders. */
|
|
61
87
|
export interface RustClientContext {
|
|
62
|
-
/** The shared lazy addon proxy. */
|
|
88
|
+
/** The shared lazy addon proxy (napi surface). */
|
|
63
89
|
addon: typeof addon
|
|
90
|
+
/** The runtime adapter seam (Bun/Node — builtins + ffi/napi transport). */
|
|
91
|
+
runtime: NativeRuntimeAdapter
|
|
64
92
|
/** Per-instance toggles (mime/hmac caching). */
|
|
65
93
|
state: { mimeCache: boolean; hmacCache: boolean }
|
|
66
94
|
mimeByText: Map<string, Uint8Array>
|
|
@@ -151,6 +179,7 @@ export function createContext(options: RustOptions): RustClientContext {
|
|
|
151
179
|
|
|
152
180
|
return {
|
|
153
181
|
addon,
|
|
182
|
+
runtime: runtimeNative,
|
|
154
183
|
state,
|
|
155
184
|
mimeByText,
|
|
156
185
|
hmacSigners,
|
package/src/rust-ffi/index.ts
CHANGED
|
@@ -26,29 +26,40 @@
|
|
|
26
26
|
// rust.packed.jsonValidBatchPacked(p); // raw packed in/out
|
|
27
27
|
// rust.configure({ rayonThreads: 8 }); // override defaults
|
|
28
28
|
|
|
29
|
-
export { createRust, rust } from './client'
|
|
30
|
-
export type { RustClient } from './client'
|
|
31
|
-
export type { RustOptions } from './options'
|
|
32
|
-
export type { RustText } from './text'
|
|
33
|
-
export type { RustBatch } from './batch'
|
|
34
|
-
export type { RustPacked } from './packed'
|
|
35
|
-
export type { SchemaValidator } from '../shared/packed'
|
|
36
29
|
export type {
|
|
37
|
-
HmacSignerInstance,
|
|
38
|
-
SchemaValidatorInstance,
|
|
39
|
-
SchemaError,
|
|
40
|
-
TemplateRendererInstance,
|
|
41
|
-
FormParserInstance,
|
|
42
|
-
MediaTypeParserInstance,
|
|
43
|
-
MediaTypeResult,
|
|
44
|
-
ConditionalRequestInstance,
|
|
45
|
-
EncodingPrefResult,
|
|
46
30
|
AcceptNegotiatorInstance,
|
|
47
31
|
Base64CodecInstance,
|
|
32
|
+
ConditionalRequestInstance,
|
|
48
33
|
CookieSignerInstance,
|
|
49
34
|
CsrfProtectorInstance,
|
|
50
|
-
|
|
35
|
+
EncodingPrefResult,
|
|
36
|
+
FormParserInstance,
|
|
37
|
+
HmacSignerInstance,
|
|
38
|
+
MediaTypeParserInstance,
|
|
39
|
+
MediaTypeResult,
|
|
51
40
|
MultipartPart,
|
|
52
41
|
PasswordHashOptions,
|
|
42
|
+
SchemaError,
|
|
43
|
+
SchemaValidatorInstance,
|
|
44
|
+
TemplateRendererInstance,
|
|
45
|
+
UrlBuilderInstance,
|
|
53
46
|
WsFrame,
|
|
54
47
|
} from '../native'
|
|
48
|
+
export type { SchemaValidator } from '../shared/packed'
|
|
49
|
+
export type { RustBatch } from './batch'
|
|
50
|
+
export type { RustClient } from './client'
|
|
51
|
+
export { createRust, rust } from './client'
|
|
52
|
+
export { proven } from './proven'
|
|
53
|
+
export {
|
|
54
|
+
PROVEN_SELECTION,
|
|
55
|
+
isProven,
|
|
56
|
+
provenEntry,
|
|
57
|
+
provenImpl,
|
|
58
|
+
provenStatus,
|
|
59
|
+
provenSummary,
|
|
60
|
+
provenSurface,
|
|
61
|
+
} from '../shared/proven'
|
|
62
|
+
export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
|
|
63
|
+
export type { RustOptions } from './options'
|
|
64
|
+
export type { RustPacked } from './packed'
|
|
65
|
+
export type { RustText } from './text'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/rust-ffi/into.ts — pooled `*Into` output-write helper (napi fallback).
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write `bytes` into a caller-owned `output` buffer, or throw when it does not
|
|
5
|
+
* fit.
|
|
6
|
+
*
|
|
7
|
+
* Shared tail of the pooled (`*Into`) napi-fallback paths: every op copies the
|
|
8
|
+
* allocating result into the caller buffer with the same capacity check, error
|
|
9
|
+
* message, and byte-length return. The bun:ffi `_into` path never reaches this
|
|
10
|
+
* helper — the C ABI streams straight into `output`.
|
|
11
|
+
*
|
|
12
|
+
* @param op Label for the error message (e.g. `"gzip compress"`).
|
|
13
|
+
* @param output The caller-owned destination buffer.
|
|
14
|
+
* @param bytes The allocating op result to copy in.
|
|
15
|
+
* @returns The number of bytes written (`bytes.length`).
|
|
16
|
+
*/
|
|
17
|
+
export function writeInto(op: string, output: Uint8Array, bytes: Uint8Array): number {
|
|
18
|
+
if (output.length < bytes.length) throw new Error(`${op}: output buffer too small`)
|
|
19
|
+
output.set(bytes)
|
|
20
|
+
return bytes.length
|
|
21
|
+
}
|
package/src/rust-ffi/options.ts
CHANGED
|
@@ -51,7 +51,7 @@ export function resolveRayonThreads(explicit?: number): number {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const envThreads = Number(
|
|
54
|
-
resolveEnvVar('CASTRUM_RAYON_THREADS'
|
|
54
|
+
resolveEnvVar('CASTRUM_RAYON_THREADS'),
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
if (Number.isFinite(envThreads) && envThreads > 0) {
|
package/src/rust-ffi/packed.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// `rust.packed.*` is the zero-copy API: callers pass already-packed buffers and
|
|
4
4
|
// receive the raw packed output, plus the batch metadata/count helpers.
|
|
5
5
|
|
|
6
|
-
import { asNumber } from './options'
|
|
7
|
-
import { resolvePoolNative, type RustClientContext } from './context'
|
|
8
6
|
import type { PasswordHashOptions } from '../native'
|
|
7
|
+
import { type RustClientContext, resolvePoolNative } from './context'
|
|
8
|
+
import { asNumber } from './options'
|
|
9
9
|
|
|
10
10
|
/** Raw packed-wire FFI namespace. */
|
|
11
11
|
export interface RustPacked {
|
package/src/rust-ffi/proven.ts
CHANGED
|
@@ -1,42 +1,28 @@
|
|
|
1
|
-
// src/rust-ffi/proven.ts —
|
|
1
|
+
// src/rust-ffi/proven.ts — the baked "proven selection" surface.
|
|
2
2
|
//
|
|
3
|
-
// `proven` exposes the FULL `rust.*` surface
|
|
4
|
-
//
|
|
5
|
-
// the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// The registry (`src/shared/proven.ts`) is the single source of truth for the
|
|
11
|
-
// annotations; `bun run check:proven` audits it against the latest benchmark
|
|
12
|
-
// report (with `--fail` to gate CI).
|
|
3
|
+
// `proven` re-exposes the FULL `rust.*` surface (identical object) alongside
|
|
4
|
+
// the pure-data registry (`src/shared/proven.ts`) that states which
|
|
5
|
+
// implementation is the benchmark-proven winner for every op: 'native',
|
|
6
|
+
// 'js', or 'bun' (Bun built-in delegation). The winners are BAKED (committed,
|
|
7
|
+
// benchmark-derived) and verified by `test/unit/shared/proven.test.ts` — no
|
|
8
|
+
// live benchmark audit and no `@deprecated` JSDoc machinery (the old
|
|
9
|
+
// `check:proven` / `check:annotate` friction, intentionally not re-added).
|
|
13
10
|
|
|
14
11
|
import { rust, type RustClient } from './client'
|
|
15
12
|
|
|
16
|
-
/**
|
|
17
|
-
* The performance-annotated client: the full {@link RustClient} surface.
|
|
18
|
-
* Each exported function's JSDoc notes its measured performance vs the JS
|
|
19
|
-
* baseline; functions slower than the baseline carry `@deprecated`.
|
|
20
|
-
*/
|
|
13
|
+
/** The full `rust.*` surface — identical to `rust`; the proven wiring is baked (see {@link ../shared/proven}). */
|
|
21
14
|
export type ProvenClient = RustClient
|
|
22
15
|
|
|
23
|
-
/**
|
|
24
|
-
* The full `rust.*` surface with benchmark-annotated JSDoc. Identical to
|
|
25
|
-
* `rust` — the annotations and `@deprecated` markers are what communicate
|
|
26
|
-
* performance now, not a curated subset.
|
|
27
|
-
*/
|
|
16
|
+
/** The full `rust.*` surface, exposed for audit. Same object as `rust`. */
|
|
28
17
|
export const proven: ProvenClient = rust
|
|
29
18
|
|
|
30
|
-
// Re-export the registry + helpers so consumers can audit the surface.
|
|
31
19
|
export {
|
|
32
|
-
|
|
33
|
-
provenStatus,
|
|
20
|
+
PROVEN_SELECTION,
|
|
34
21
|
isProven,
|
|
35
|
-
|
|
22
|
+
provenEntry,
|
|
23
|
+
provenImpl,
|
|
24
|
+
provenStatus,
|
|
36
25
|
provenSummary,
|
|
26
|
+
provenSurface,
|
|
37
27
|
} from '../shared/proven'
|
|
38
|
-
export type {
|
|
39
|
-
PerformanceStatus,
|
|
40
|
-
ProvenEntry,
|
|
41
|
-
ProvenKey,
|
|
42
|
-
} from '../shared/proven'
|
|
28
|
+
export type { ProvenEntry, ProvenImpl, ProvenStatus } from '../shared/proven'
|