castrum 0.9.4 → 0.9.7
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/Cargo.lock +28 -44
- package/Cargo.toml +21 -4
- package/README.md +8 -6
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +4460 -3648
- package/docs/API.md +1 -0
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CASE_STUDY.md +4 -2
- package/docs/ENVIRONMENT.md +9 -2
- package/docs/FFI_BUN_GUIDE.md +166 -9
- package/docs/HOW_TO_ADD_AN_OP.md +34 -0
- package/docs/REPO_MAP.md +16 -5
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +5 -0
- package/package.json +172 -163
- package/rust/crypto/pbkdf2.rs +9 -0
- package/rust/crypto/session.rs +164 -7
- package/rust/ffi/crypto.rs +119 -29
- package/rust/ffi/mod.rs +1 -0
- package/rust/ffi/task.rs +172 -0
- package/rust/ffi/tests.rs +173 -0
- package/rust/http/form.rs +6 -2
- package/rust/http/query_parser.rs +53 -22
- package/rust/ingress/native_route.rs +131 -140
- package/rust/ingress/pipeline.rs +3 -3
- package/rust/ingress/tests.rs +5 -4
- package/rust/json/json_ser.rs +13 -11
- package/rust/lib.rs +1 -0
- package/rust/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -0
- package/rust/util/bytes.rs +238 -51
- package/src/ingress/health.ts +1 -2
- package/src/native/ffi/build/codecs.ts +29 -7
- package/src/native/ffi/build/compress.ts +8 -0
- package/src/native/ffi/build/instances.ts +54 -2
- package/src/native/ffi/build/metrics.ts +23 -7
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +18 -0
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +3 -1
- package/src/native/ffi/types.ts +57 -0
- package/src/native/ffi.ts +85 -14
- package/src/native/loader.ts +53 -3
- package/src/rust-ffi/scalar/http.ts +54 -16
- package/src/rust-ffi/text.ts +25 -3
- package/src/shared/bytes.ts +19 -0
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- package/src/task/runtime.ts +568 -0
|
@@ -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
|
+
}
|