castrum 0.9.3 → 0.9.6

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 (206) hide show
  1. package/Cargo.lock +2242 -0
  2. package/Cargo.toml +155 -0
  3. package/README.md +14 -6
  4. package/build.rs +5 -0
  5. package/castrum.darwin-arm64.node +0 -0
  6. package/castrum.darwin-x64.node +0 -0
  7. package/castrum.linux-arm64-gnu.node +0 -0
  8. package/castrum.linux-arm64-musl.node +0 -0
  9. package/castrum.linux-x64-gnu.node +0 -0
  10. package/castrum.linux-x64-musl.node +0 -0
  11. package/castrum.linux-x64-v3-gnu.node +0 -0
  12. package/castrum.win32-x64-msvc.node +0 -0
  13. package/dist/index.d.ts +1508 -1291
  14. package/dist/index.js +3391 -2069
  15. package/docs/API.md +2 -1
  16. package/docs/ARCHITECTURE.md +3 -2
  17. package/docs/CASE_STUDY.md +5 -3
  18. package/docs/ENVIRONMENT.md +25 -2
  19. package/docs/FFI_BUN_GUIDE.md +242 -14
  20. package/docs/HOW_TO_ADD_AN_OP.md +10 -0
  21. package/docs/REPO_MAP.md +17 -6
  22. package/docs/RND-CONCURRENCY.md +772 -0
  23. package/docs/bun-builtins-decision-matrix.md +4 -1
  24. package/index.ts +24 -25
  25. package/package.json +173 -156
  26. package/rust/crypto/aead.rs +378 -0
  27. package/rust/crypto/argon2.rs +265 -0
  28. package/rust/crypto/base64.rs +420 -0
  29. package/rust/crypto/bcrypt.rs +89 -0
  30. package/rust/crypto/cookie_sign.rs +169 -0
  31. package/rust/crypto/csrf.rs +106 -0
  32. package/rust/crypto/ed25519.rs +215 -0
  33. package/rust/crypto/hashing.rs +140 -0
  34. package/rust/crypto/hmac_sha256.rs +88 -0
  35. package/rust/crypto/jwt/api.rs +286 -0
  36. package/rust/crypto/jwt/mod.rs +22 -0
  37. package/rust/crypto/jwt/tests.rs +312 -0
  38. package/rust/crypto/jwt/token.rs +360 -0
  39. package/rust/crypto/mod.rs +27 -0
  40. package/rust/crypto/pbkdf2.rs +105 -0
  41. package/rust/crypto/random_token.rs +85 -0
  42. package/rust/crypto/session.rs +222 -0
  43. package/rust/ffi/crypto.rs +708 -0
  44. package/rust/ffi/hashing.rs +285 -0
  45. package/rust/ffi/http.rs +752 -0
  46. package/rust/ffi/ingress.rs +303 -0
  47. package/rust/ffi/json.rs +517 -0
  48. package/rust/ffi/jwt.rs +419 -0
  49. package/rust/ffi/metrics.rs +373 -0
  50. package/rust/ffi/mod.rs +69 -0
  51. package/rust/ffi/payload.rs +275 -0
  52. package/rust/ffi/probe.rs +60 -0
  53. package/rust/ffi/rate_limit.rs +77 -0
  54. package/rust/ffi/route.rs +85 -0
  55. package/rust/ffi/task.rs +172 -0
  56. package/rust/ffi/tests.rs +2333 -0
  57. package/rust/ffi/util.rs +152 -0
  58. package/rust/ffi/validators.rs +123 -0
  59. package/rust/http/accept.rs +606 -0
  60. package/rust/http/cookie_parser.rs +140 -0
  61. package/rust/http/etag.rs +289 -0
  62. package/rust/http/form.rs +136 -0
  63. package/rust/http/headers.rs +267 -0
  64. package/rust/http/http_date.rs +286 -0
  65. package/rust/http/http_parser.rs +252 -0
  66. package/rust/http/media_type.rs +355 -0
  67. package/rust/http/method.rs +200 -0
  68. package/rust/http/mime_lookup.rs +130 -0
  69. package/rust/http/mod.rs +32 -0
  70. package/rust/http/multipart.rs +685 -0
  71. package/rust/http/query_parser.rs +211 -0
  72. package/rust/http/url_codec.rs +296 -0
  73. package/rust/http/url_join.rs +324 -0
  74. package/rust/ingress/api.rs +355 -0
  75. package/rust/ingress/cors.rs +403 -0
  76. package/rust/ingress/ingress_constants.rs +209 -0
  77. package/rust/ingress/ip_trust.rs +312 -0
  78. package/rust/ingress/mod.rs +48 -0
  79. package/rust/ingress/native_route.rs +770 -0
  80. package/rust/ingress/options.rs +176 -0
  81. package/rust/ingress/output.rs +406 -0
  82. package/rust/ingress/packed.rs +191 -0
  83. package/rust/ingress/pipeline.rs +534 -0
  84. package/rust/ingress/proxy.rs +138 -0
  85. package/rust/ingress/rate_limit.rs +538 -0
  86. package/rust/ingress/terminal.rs +114 -0
  87. package/rust/ingress/tests.rs +567 -0
  88. package/rust/ingress/time.rs +70 -0
  89. package/rust/json/fast_schema/capture.rs +336 -0
  90. package/rust/json/fast_schema/compile.rs +556 -0
  91. package/rust/json/fast_schema/cursor.rs +368 -0
  92. package/rust/json/fast_schema/email.rs +282 -0
  93. package/rust/json/fast_schema/errors.rs +247 -0
  94. package/rust/json/fast_schema/mod.rs +57 -0
  95. package/rust/json/fast_schema/tests.rs +1143 -0
  96. package/rust/json/fast_schema/types.rs +140 -0
  97. package/rust/json/fast_schema/validate.rs +931 -0
  98. package/rust/json/json_ops.rs +121 -0
  99. package/rust/json/json_schema.rs +837 -0
  100. package/rust/json/json_ser.rs +883 -0
  101. package/rust/json/mod.rs +19 -0
  102. package/rust/json/napi_marshal.rs +318 -0
  103. package/rust/json/patch/api.rs +60 -0
  104. package/rust/json/patch/engine.rs +153 -0
  105. package/rust/json/patch/mod.rs +47 -0
  106. package/rust/json/patch/ops.rs +270 -0
  107. package/rust/json/patch/pointer.rs +49 -0
  108. package/rust/json/patch/tests.rs +499 -0
  109. package/rust/lib.rs +73 -0
  110. package/rust/metrics/api.rs +148 -0
  111. package/rust/metrics/mod.rs +21 -0
  112. package/rust/metrics/registry.rs +961 -0
  113. package/rust/panic_safety.rs +40 -0
  114. package/rust/payload/compress.rs +512 -0
  115. package/rust/payload/mod.rs +13 -0
  116. package/rust/payload/sse.rs +214 -0
  117. package/rust/payload/template.rs +253 -0
  118. package/rust/payload/websocket.rs +69 -0
  119. package/rust/payload/ws_frames.rs +354 -0
  120. package/rust/proptest_suite.rs +139 -0
  121. package/rust/selection.rs +95 -0
  122. package/rust/src/bin/native-bench.rs +495 -0
  123. package/rust/task/completion.rs +119 -0
  124. package/rust/task/mod.rs +65 -0
  125. package/rust/task/ops.rs +503 -0
  126. package/rust/task/runtime.rs +220 -0
  127. package/rust/task/tests.rs +231 -0
  128. package/rust/test_support.rs +71 -0
  129. package/rust/util/batch/api.rs +266 -0
  130. package/rust/util/batch/core.rs +473 -0
  131. package/rust/util/batch/mod.rs +23 -0
  132. package/rust/util/batch/tests.rs +445 -0
  133. package/rust/util/batch_core.rs +184 -0
  134. package/rust/util/bytes.rs +594 -0
  135. package/rust/util/mod.rs +34 -0
  136. package/rust/util/packed.rs +473 -0
  137. package/rust/util/text.rs +130 -0
  138. package/rust/util/threadpool.rs +128 -0
  139. package/rust/util/validation.rs +419 -0
  140. package/rust-toolchain.toml +17 -0
  141. package/scripts/postinstall.mjs +199 -0
  142. package/src/ingress/constants.ts +1 -1
  143. package/src/ingress/context.ts +2 -2
  144. package/src/ingress/decode/baked-result.ts +13 -13
  145. package/src/ingress/decode/fast-result.ts +17 -17
  146. package/src/ingress/errors.ts +7 -7
  147. package/src/ingress/headers/baked-templates.ts +1 -1
  148. package/src/ingress/headers/fast-templates.ts +3 -3
  149. package/src/ingress/health.ts +1 -2
  150. package/src/ingress/index.ts +1 -1
  151. package/src/ingress/native-route.ts +9 -5
  152. package/src/ingress/options.ts +1 -1
  153. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  154. package/src/ingress/packing/route-wire.ts +1 -5
  155. package/src/ingress/response/error-bodies.ts +5 -5
  156. package/src/ingress/response/terminal.ts +1 -1
  157. package/src/ingress/routes/echo.ts +4 -4
  158. package/src/ingress/routes/fallback.ts +2 -2
  159. package/src/ingress/routes/head.ts +2 -2
  160. package/src/ingress/routes/native.ts +2 -1
  161. package/src/ingress/routes/options.ts +1 -1
  162. package/src/ingress/routes/read.ts +2 -2
  163. package/src/ingress/server.ts +10 -6
  164. package/src/ingress/status.ts +7 -7
  165. package/src/integration/batch.ts +1 -1
  166. package/src/integration/index.ts +9 -9
  167. package/src/integration/pipeline.ts +6 -6
  168. package/src/integration/websocket.ts +2 -2
  169. package/src/loader/index.ts +1 -5
  170. package/src/loader/ops.ts +1 -1
  171. package/src/native/ffi/build/codecs.ts +130 -12
  172. package/src/native/ffi/build/compress.ts +22 -10
  173. package/src/native/ffi/build/instances.ts +46 -4
  174. package/src/native/ffi/build/metrics.ts +247 -0
  175. package/src/native/ffi/build/parse.ts +59 -23
  176. package/src/native/ffi/build/task.ts +79 -0
  177. package/src/native/ffi/build/util.ts +32 -1
  178. package/src/native/ffi/build.ts +4 -0
  179. package/src/native/ffi/selftest.ts +12 -3
  180. package/src/native/ffi/types.ts +151 -9
  181. package/src/native/ffi.ts +160 -15
  182. package/src/native/index.ts +1 -1
  183. package/src/native/loader.ts +55 -5
  184. package/src/native/types/instances.ts +34 -0
  185. package/src/native/types.ts +11 -0
  186. package/src/runtime/codec.ts +53 -9
  187. package/src/runtime/types.ts +7 -0
  188. package/src/rust-ffi/batch/build.ts +16 -9
  189. package/src/rust-ffi/index.ts +7 -6
  190. package/src/rust-ffi/options.ts +1 -3
  191. package/src/rust-ffi/proven.ts +3 -3
  192. package/src/rust-ffi/scalar/crypto.ts +1 -1
  193. package/src/rust-ffi/scalar/factories.ts +73 -0
  194. package/src/rust-ffi/scalar/hashing.ts +1 -1
  195. package/src/rust-ffi/scalar/http.ts +71 -27
  196. package/src/rust-ffi/scalar/index.ts +2 -0
  197. package/src/rust-ffi/scalar/interface.ts +20 -0
  198. package/src/rust-ffi/scalar/payload.ts +11 -3
  199. package/src/rust-ffi/scalar/validation.ts +121 -0
  200. package/src/rust-ffi/text.ts +25 -3
  201. package/src/selection.ts +1 -1
  202. package/src/shared/bytes.ts +24 -4
  203. package/src/shared/codec.ts +18 -3
  204. package/src/task/index.ts +24 -0
  205. package/src/task/op.ts +171 -0
  206. package/src/task/runtime.ts +568 -0
@@ -0,0 +1,121 @@
1
+ // src/rust-ffi/scalar/validation.ts — Validation & text-utility scalar methods.
2
+ //
3
+ // Mirrors rust/util/validation.rs (batch fixed-width hex validation) and
4
+ // rust/util/text.rs (JS-RegExp metacharacter escaping). Stateless; on Bun the
5
+ // string paths cross as cstring ARGs (engine-transcoded — zero TextEncoder)
6
+ // and regexEscape returns via a cstring return (engine-cloned — zero decode).
7
+
8
+ import { encoder, toBytes } from '../../shared/bytes'
9
+ import { decodeUtf8 } from '../../shared/codec'
10
+ import type { RustClientContext } from '../context'
11
+ import { memoizeFfi, resolveNative } from '../context'
12
+
13
+ /**
14
+ * Non-zeroing output allocation for the batch verdicts (`Buffer.allocUnsafe`
15
+ * skips the 4 KB memset a plain `new Uint8Array` pays; every byte is
16
+ * overwritten by the native write anyway).
17
+ */
18
+ const allocOut = (n: number): Uint8Array =>
19
+ typeof Buffer !== 'undefined' && typeof Buffer.allocUnsafe === 'function'
20
+ ? Buffer.allocUnsafe(n)
21
+ : new Uint8Array(n)
22
+
23
+ /** Validation / text-utility methods (`Pick<RustScalar, ...>`). */
24
+ export function buildValidation(ctx: RustClientContext) {
25
+ const { transport } = ctx.runtime
26
+ const n = (name: string) => resolveNative(ctx, name)
27
+ // Lazy-memoized ffi surface: binds on first call (no eager dlopen at import).
28
+ const ffi = memoizeFfi(transport)
29
+
30
+ return {
31
+ /**
32
+ * Validate NEWLINE-separated strings as fixed-width hex (e.g. Mongo
33
+ * ObjectIds at width 24) in ONE native call → one verdict byte per line.
34
+ * Replaces the per-item `/^[0-9a-f]{24}$/` regex loops list endpoints
35
+ * scatter everywhere. Accepts an array of id strings directly — the join
36
+ * crosses as a `cstring` ARG, so the JS side never touches an encoder.
37
+ *
38
+ * @param input Newline-separated candidates as bytes, or an array of
39
+ * candidate strings (joined internally with `\n`)
40
+ * @param width Exact expected length of each line (1..=4096)
41
+ * @returns Uint8Array of 1/0 verdicts, one per line (trailing newline
42
+ * does not add an extra verdict)
43
+ * @throws On `width` outside 1..=4096
44
+ * @example
45
+ * ```ts
46
+ * rust.hexValidateBatch(['507f1f77bcf86cd799439011', 'nope'], 24) // [1, 0]
47
+ * ```
48
+ */
49
+ hexValidateBatch(input: Uint8Array | readonly string[], width: number): Uint8Array {
50
+ if (width === 0 || width > 4096 || !Number.isInteger(width)) {
51
+ throw new Error('hex validate batch: width must be an integer 1..=4096')
52
+ }
53
+ const f = ffi()
54
+ if (f) {
55
+ // Verdict count ≤ line count ≤ input length — one native pass.
56
+ const joined =
57
+ typeof input === 'string'
58
+ ? input
59
+ : Array.isArray(input)
60
+ ? (input as readonly string[]).join('\n')
61
+ : null
62
+ let out = allocOut(Math.min(joined !== null ? joined.length + 1 : input.length + 1, 4096))
63
+ let w =
64
+ joined !== null
65
+ ? f.hexValidateBatchStr(joined, width, out)
66
+ : f.hexValidateBatchInto(input as Uint8Array, width, out)
67
+ if (w > out.length) {
68
+ out = new Uint8Array(w)
69
+ w =
70
+ joined !== null
71
+ ? f.hexValidateBatchStr(joined, width, out)
72
+ : f.hexValidateBatchInto(input as Uint8Array, width, out)
73
+ }
74
+ return out.subarray(0, w)
75
+ }
76
+ const bytes = Array.isArray(input)
77
+ ? encoder.encode((input as readonly string[]).join('\n'))
78
+ : (input as Uint8Array)
79
+ return n('hexValidateBatch')(bytes, width) as Uint8Array
80
+ },
81
+
82
+ /**
83
+ * Escape JS-RegExp metacharacters (`\ . * + ? ^ $ { } | ( ) [ ]`) so a
84
+ * pattern built with `new RegExp(rust.regexEscape(userInput), 'i')`
85
+ * matches the input LITERALLY. Under Bun the whole call is the FFI
86
+ * crossing + the escape: the input crosses as a `cstring` ARG (engine-
87
+ * transcoded, zero encode) and the result returns as a `cstring`
88
+ * (engine-cloned, zero decode).
89
+ *
90
+ * @param input Arbitrary text or bytes (multi-byte UTF-8 passes through)
91
+ * @returns The escaped pattern string
92
+ * @example
93
+ * ```ts
94
+ * const re = new RegExp(rust.regexEscape('a.c*(x)'), 'i')
95
+ * re.test('A.C(X)') // true — literal match, not wildcard semantics
96
+ * ```
97
+ */
98
+ regexEscape(input: Uint8Array | string): string {
99
+ const f = ffi()
100
+ if (f) {
101
+ if (typeof input === 'string') {
102
+ const escaped = f.regexEscapeStr(input)
103
+ if (escaped === null) throw new Error('regex escape failed')
104
+ return escaped
105
+ }
106
+ // Bytes path: needed-size convention with a generous first guess.
107
+ const bytes = toBytes(input)
108
+ let out = new Uint8Array(bytes.length + 16)
109
+ let w = f.regexEscapeInto(bytes, out)
110
+ if (w > out.length) {
111
+ out = new Uint8Array(w)
112
+ w = f.regexEscapeInto(bytes, out)
113
+ }
114
+ return decodeUtf8(out.subarray(0, w))
115
+ }
116
+ // napi takes/returns strings directly.
117
+ const s = typeof input === 'string' ? input : decodeUtf8(toBytes(input))
118
+ return n('regexEscape')(s) as string
119
+ },
120
+ }
121
+ }
@@ -1,16 +1,27 @@
1
1
  // src/rust-ffi/text.ts — String-oriented FFI namespace (text in → text out).
2
2
  //
3
3
  // `rust.text.*` is the ergonomic string API over the byte-level native
4
- // functions. Pure wrappers: encode input, call native, decode output.
4
+ // functions. MOST of it is FFI-first with `cstring` args/returns: the engine
5
+ // transcodes a JS string in-call, so there is no JS-side encode at all (and
6
+ // byte-arg ops are not involved). Encode/decode wrappers exist only for the
7
+ // napi fallback and for ops with no cstring form — plus a NUL guard wherever a
8
+ // `cstring` ARG could truncate a verdict or a byte-exact value
9
+ // (docs/FFI_BUN_GUIDE.md §6.1).
5
10
  //
6
11
  // Runtime dispatch is centralized in the adapter (`ctx.runtime`): the Bun
7
12
  // built-in delegations (urlEncode/urlDecode) come from `builtins.has(op)` and
8
13
  // the native call from `transport.ffi` / `transport.resolve` (bun:ffi first,
9
14
  // napi fallback) — no inline `isBun()` / `getBunFFI()`.
10
15
 
11
- import { decoder, encoder } from '../shared/bytes'
16
+ import { decoder, encoder, hasNul } from '../shared/bytes'
12
17
  import { type RustClientContext, resolveNative } from './context'
13
18
 
19
+ /**
20
+ * Scratch for the byte-path `Sec-WebSocket-Accept` fallback (28 chars; the
21
+ * NUL case only). Decoded synchronously before returning, so reuse is safe.
22
+ */
23
+ const WS_ACCEPT_OUT = new Uint8Array(64)
24
+
14
25
  /** String-oriented FFI namespace. */
15
26
  export interface RustText {
16
27
  /**
@@ -91,8 +102,19 @@ export function buildText(ctx: RustClientContext): RustText {
91
102
  // FFI-first: `cstring` ARG + cstring return (native transfer both ways —
92
103
  // the engine transcodes the key in-engine, no JS encode). napi keeps the
93
104
  // encode/decode path.
105
+ //
106
+ // Exception: a NUL would truncate the `cstring` ARG, so the accept value
107
+ // would be computed for a PREFIX of the key. Encode and take the byte path
108
+ // in that (practically unreachable) case: still a wrong key for a client
109
+ // that sent a NUL, but wrong for the bytes the caller actually passed.
94
110
  const f = transport.ffi
95
- if (f) return f.wsAcceptKey(key)
111
+ if (f) {
112
+ if (!hasNul(key)) return f.wsAcceptKey(key)
113
+ const w = f.wsAcceptKeyInto(encoder.encode(key), WS_ACCEPT_OUT)
114
+ return w > 0 && w <= WS_ACCEPT_OUT.length
115
+ ? decoder.decode(WS_ACCEPT_OUT.subarray(0, w))
116
+ : ''
117
+ }
96
118
  return decoder.decode(resolveNative(ctx, 'wsAcceptKey')(encoder.encode(key)) as Uint8Array)
97
119
  },
98
120
  validateEmail(input) {
package/src/selection.ts CHANGED
@@ -18,8 +18,8 @@
18
18
  // live wiring, so it can't drift.
19
19
 
20
20
  import { getAddon, lazyAddon } from './native'
21
- import { provenImpl } from './shared/proven'
22
21
  import { runtimeNative } from './runtime/native'
22
+ import { provenImpl } from './shared/proven'
23
23
 
24
24
  /** Recommended implementation for an operation. */
25
25
  export type OpImpl = 'native' | 'js'
@@ -4,10 +4,11 @@ import { decodeUtf8, encodeUtf8, encodeUtf8Into } from './codec'
4
4
 
5
5
  /**
6
6
  * Shared UTF-8 encoder (encode / encodeInto), backed by the runtime-native
7
- * codec (`src/shared/codec.ts`): on Bun the work is done by Bun's native
8
- * transfer machinery (`Bun.ArrayBufferSink` / a zero-copy `Buffer` view
9
- * write); under Node it falls back to `TextEncoder`. Reusing a single object
10
- * avoids allocation overhead in hot paths.
7
+ * codec (`src/shared/codec.ts`): a shared native `TextEncoder` singleton on
8
+ * both runtimes (on Bun 1.4 the engine-native implementation measures ~3x
9
+ * faster than the old `Bun.ArrayBufferSink` path) plus a zero-copy `Buffer`
10
+ * view write for `encodeInto`. Reusing a single object avoids allocation
11
+ * overhead in hot paths.
11
12
  *
12
13
  * `encode` accepts the string-or-bytes union the text-returning `rust.*` ops
13
14
  * surface (Bun returns strings, Node returns bytes) and normalizes to bytes —
@@ -133,3 +134,22 @@ export function toPlainBuffer(bytes: Uint8Array): Uint8Array {
133
134
  copy.set(bytes)
134
135
  return copy
135
136
  }
137
+
138
+ /**
139
+ * Does this string contain a NUL (`U+0000`)?
140
+ *
141
+ * `bun:ffi` transcodes a JS string passed to a `cstring` ARG into a
142
+ * NUL-TERMINATED UTF-8 buffer, so an embedded NUL SILENTLY TRUNCATES the value
143
+ * natively (Bun's docs describe the transcode; the truncation is the C
144
+ * convention it inherits). That is fine for developer-supplied config, and a
145
+ * real bug wherever the callee answers with a VERDICT (a truncated prefix
146
+ * passes where the full input fails) or where the exact bytes ARE the result.
147
+ *
148
+ * Cheap: one `indexOf` scan, ~4 ns for typical inputs.
149
+ *
150
+ * @param s - The string to check.
151
+ * @returns `true` when a NUL is present.
152
+ */
153
+ export function hasNul(s: string): boolean {
154
+ return s.indexOf('\u0000') !== -1
155
+ }
@@ -12,9 +12,10 @@ import { runtimeCodec } from '../runtime/codec'
12
12
  /**
13
13
  * Encode a string to UTF-8 bytes.
14
14
  *
15
- * On Bun this uses `Bun.ArrayBufferSink` (native UTF-8 writing — no
16
- * `TextEncoder`). Under Node it falls back to the standard `TextEncoder`.
17
- * The implementation is selected once at load via the runtime adapter.
15
+ * Both runtimes use a shared native `TextEncoder` singleton (on Bun 1.4 the
16
+ * engine-native implementation measures ~3x faster than the previous
17
+ * `Bun.ArrayBufferSink` start/write/flush path). The implementation is
18
+ * selected once at load via the runtime adapter.
18
19
  *
19
20
  * @param s - The string to encode to UTF-8 bytes.
20
21
  * @returns A fresh `Uint8Array` of the UTF-8 encoding of `s`.
@@ -64,6 +65,20 @@ export const encodeUtf8Into: (s: string, dest: Uint8Array, offset?: number) => n
64
65
  */
65
66
  export const decodeUtf8: (bytes: Uint8Array) => string = runtimeCodec.decodeUtf8
66
67
 
68
+ /**
69
+ * Decode the byte RANGE `[start, end)` of `bytes` to a string (replacement
70
+ * mode on invalid). Zero-copy ranged decode for packed-wire unpackers — no
71
+ * per-field `subarray` view. ASCII ranges take a latin1 fast path (~2x on
72
+ * Bun); multi-byte ranges fall back to real UTF-8 decoding of that range.
73
+ *
74
+ * @param bytes - The backing byte buffer.
75
+ * @param start - Range start (inclusive, relative to the VIEW).
76
+ * @param end - Range end (exclusive, relative to the VIEW).
77
+ * @returns The decoded string.
78
+ */
79
+ export const decodeUtf8Range: (bytes: Uint8Array, start: number, end: number) => string =
80
+ runtimeCodec.decodeUtf8Range
81
+
67
82
  /**
68
83
  * Decode UTF-8 bytes to a string, throwing on invalid UTF-8.
69
84
  *
@@ -0,0 +1,24 @@
1
+ // src/task/index.ts — public barrel for the off-thread task runtime.
2
+
3
+ export {
4
+ TASK_OP,
5
+ TASK_STATUS_CANCELLED,
6
+ TASK_STATUS_ERROR,
7
+ TASK_STATUS_OK,
8
+ brotliCapacityGuess,
9
+ encodeArgon2VerifyArgs,
10
+ encodeBrotliDecompressArgs,
11
+ encodeGzipCompressHeader,
12
+ encodeGzipDecompressArgs,
13
+ encodePbkdf2Args,
14
+ gzipCompressUpperBound,
15
+ } from './op'
16
+ export type { TaskOpName } from './op'
17
+ export { createTaskRuntime } from './runtime'
18
+ export type {
19
+ Pbkdf2RunOptions,
20
+ TaskRunOptions,
21
+ TaskRuntime,
22
+ TaskRuntimeOptions,
23
+ TaskStats,
24
+ } from './runtime'
package/src/task/op.ts ADDED
@@ -0,0 +1,171 @@
1
+ // src/task/op.ts — pure task-op registry: numeric ids + packed-arg encoders.
2
+ //
3
+ // PURE by design (no addon, no module state) so the wire encoding is directly
4
+ // unit-testable. The ids MUST match the `OP_*` constants in `rust/task/ops.rs`;
5
+ // `test/unit/task/runtime.test.ts` pins the round-trip.
6
+
7
+ /** Numeric op ids — mirror `rust/task/ops.rs`. */
8
+ export const TASK_OP = {
9
+ /** args `[u32 maxDecompressed LE][gzip bytes]` (0 = native 64 MiB default). */
10
+ gzipDecompress: 1,
11
+ /** args `[u32 rounds][u32 dkLen][u32 saltLen][salt][password]`. */
12
+ pbkdf2Sha256: 2,
13
+ /** `gzipDecompress` writing into a caller buffer (zero-copy output). */
14
+ gzipDecompressInto: 3,
15
+ /** args `[u32 pwLen][u32 phcLen][password][phc]` → body is one byte (1 = match). */
16
+ argon2Verify: 4,
17
+ /** args `[u32 maxDecompressed LE][brotli bytes]` (0 = native 64 MiB default). */
18
+ brotliDecompress: 5,
19
+ /** `brotliDecompress` writing into a caller buffer (zero-copy output). */
20
+ brotliDecompressInto: 6,
21
+ /** args `[u32 level][gzip bytes]` (level clamped to 0-9 natively). */
22
+ gzipCompress: 7,
23
+ } as const
24
+
25
+ /** A task op name (`gzipDecompress`). */
26
+ export type TaskOpName = keyof typeof TASK_OP
27
+
28
+ /** Completion status codes (mirror `rust/task/ops.rs`). */
29
+ export const TASK_STATUS_OK = 0
30
+ /** The op returned an error; the body is the UTF-8 error message. */
31
+ export const TASK_STATUS_ERROR = 1
32
+ /** The task was cancelled before/while it ran; the body is empty. */
33
+ export const TASK_STATUS_CANCELLED = 2
34
+ /** The zero-copy destination was too small; the body is the exact size (u64 LE). */
35
+ export const TASK_STATUS_TOO_SMALL = 3
36
+ /** The op has no zero-copy form; fall back to the copy path. */
37
+ export const TASK_STATUS_UNSUPPORTED = 4
38
+
39
+ /** Shared zero-length payload (header-only ops). */
40
+ const EMPTY = new Uint8Array(0)
41
+
42
+ // Shared `[u32 header][bytes]` packing for the single-header codec ops (the
43
+ // `maxDecompressed` cap for decompress, the level for compress).
44
+ function packHeaderAndBytes(header: number, data: Uint8Array, out?: Uint8Array): Uint8Array {
45
+ const need = 4 + data.length
46
+ const dest = out !== undefined && out.length >= need ? out : new Uint8Array(need)
47
+ // View-aware: `out` may be a subarray with a non-zero byteOffset.
48
+ new DataView(dest.buffer, dest.byteOffset, need).setUint32(0, header >>> 0, true)
49
+ dest.set(data, 4)
50
+ return dest.subarray(0, need)
51
+ }
52
+
53
+ /**
54
+ * Pack the `gzip.decompress` task args: `[u32 maxDecompressed LE][gzip bytes]`.
55
+ *
56
+ * @param data Compressed gzip bytes.
57
+ * @param maxDecompressed Output cap in bytes; `undefined`/`0` selects the
58
+ * native 64 MiB decompression-bomb default.
59
+ * @param out Optional caller-owned destination (>= `4 + data.length`),
60
+ * returned as a view. Native copies args on submit, so the
61
+ * same buffer may be reused for the next submission — but
62
+ * only while the caller stays SYNCHRONOUS (a zero-copy
63
+ * retry re-submits after an await and must own its memory).
64
+ * @returns A packed args buffer.
65
+ */
66
+ export function encodeGzipDecompressArgs(
67
+ data: Uint8Array,
68
+ maxDecompressed?: number,
69
+ out?: Uint8Array,
70
+ ): Uint8Array {
71
+ return packHeaderAndBytes(maxDecompressed ?? 0, data, out)
72
+ }
73
+
74
+ /**
75
+ * Pack the `brotli.decompress` task args: `[u32 maxDecompressed LE][bytes]`.
76
+ *
77
+ * @param data Compressed brotli bytes.
78
+ * @param maxDecompressed Output cap in bytes (`undefined`/`0` → native 64 MiB).
79
+ * @param out Optional caller-owned destination (see the gzip form).
80
+ * @returns A packed args buffer.
81
+ */
82
+ export function encodeBrotliDecompressArgs(
83
+ data: Uint8Array,
84
+ maxDecompressed?: number,
85
+ out?: Uint8Array,
86
+ ): Uint8Array {
87
+ return packHeaderAndBytes(maxDecompressed ?? 0, data, out)
88
+ }
89
+
90
+ /**
91
+ * Pack the `gzip.compress` task HEADER: `[u32 level]`. The payload is passed
92
+ * separately so it can be read in place (zero-copy input) instead of copied.
93
+ *
94
+ * @param level Deflate level (`0`-`9`, clamped natively).
95
+ * @param out Optional caller-owned destination (>= 4 bytes).
96
+ * @returns The 4-byte header.
97
+ */
98
+ export function encodeGzipCompressHeader(level = 6, out?: Uint8Array): Uint8Array {
99
+ return packHeaderAndBytes(level, EMPTY, out)
100
+ }
101
+
102
+ /**
103
+ * Pack the `argon2id.verify` task args:
104
+ * `[u32 pwLen][u32 phcLen][password][phc]`. No `out` parameter on purpose: a
105
+ * shared scratch would keep a plaintext password alive far longer than a
106
+ * per-call allocation, which is the wrong trade for a secret.
107
+ *
108
+ * @param password Candidate password bytes.
109
+ * @param phc PHC string bytes (`$argon2id$...`).
110
+ * @returns A packed args buffer owned by the caller (Rust copies it on submit).
111
+ */
112
+ export function encodeArgon2VerifyArgs(password: Uint8Array, phc: Uint8Array): Uint8Array {
113
+ const out = new Uint8Array(8 + password.length + phc.length)
114
+ const view = new DataView(out.buffer)
115
+ view.setUint32(0, password.length >>> 0, true)
116
+ view.setUint32(4, phc.length >>> 0, true)
117
+ out.set(password, 8)
118
+ out.set(phc, 8 + password.length)
119
+ return out
120
+ }
121
+
122
+ /**
123
+ * Safe upper bound on the gzip size of `n` input bytes: deflate's worst case is
124
+ * stored blocks (5 bytes per block) plus the 18-byte gzip wrapper. The 16 KiB
125
+ * block assumption is deliberately conservative (real blocks are 64 KiB), so a
126
+ * destination sized with this can never be too small — no retry round trip.
127
+ *
128
+ * @param n Uncompressed input length in bytes.
129
+ * @returns An upper bound in bytes.
130
+ */
131
+ export function gzipCompressUpperBound(n: number): number {
132
+ return n + 5 * (Math.ceil(n / 16384) + 1) + 18
133
+ }
134
+
135
+ /**
136
+ * Starting capacity for a brotli zero-copy destination. Brotli carries no
137
+ * ISIZE trailer, so there is no exact hint: this is a ratio guess, and the
138
+ * needed-size retry lands the exact size if it falls short.
139
+ *
140
+ * @param compressedLen Compressed input length in bytes.
141
+ * @returns A capacity guess in bytes (floored at the runtime's zero-copy size).
142
+ */
143
+ export function brotliCapacityGuess(compressedLen: number): number {
144
+ return Math.max(64 * 1024, compressedLen * 8)
145
+ }
146
+
147
+ /**
148
+ * Pack the `pbkdf2.sha256` task args:
149
+ * `[u32 rounds][u32 dkLen][u32 saltLen][salt][password]`.
150
+ *
151
+ * @param password Password bytes (the PBKDF2 secret).
152
+ * @param salt Salt bytes.
153
+ * @param rounds Iteration count (clamped to >= 1 natively).
154
+ * @param dkLen Derived-key length in bytes (default 32, capped at 1 MiB).
155
+ * @returns A packed args buffer owned by the caller (Rust copies it on submit).
156
+ */
157
+ export function encodePbkdf2Args(
158
+ password: Uint8Array,
159
+ salt: Uint8Array,
160
+ rounds: number,
161
+ dkLen = 32,
162
+ ): Uint8Array {
163
+ const out = new Uint8Array(12 + salt.length + password.length)
164
+ const view = new DataView(out.buffer)
165
+ view.setUint32(0, rounds >>> 0, true)
166
+ view.setUint32(4, dkLen >>> 0, true)
167
+ view.setUint32(8, salt.length >>> 0, true)
168
+ out.set(salt, 12)
169
+ out.set(password, 12 + salt.length)
170
+ return out
171
+ }