typesea 0.2.0 → 0.3.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/CHANGELOG.md +40 -0
- package/README.md +104 -41
- package/SECURITY.md +52 -0
- package/dist/aot/index.d.ts +1 -1
- package/dist/aot/index.d.ts.map +1 -1
- package/dist/aot/index.js +22 -3
- package/dist/builders/composite.d.ts +6 -3
- package/dist/builders/composite.d.ts.map +1 -1
- package/dist/builders/composite.js +22 -13
- package/dist/builders/index.d.ts +6 -5
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +5 -4
- package/dist/builders/modifier.d.ts +6 -0
- package/dist/builders/modifier.d.ts.map +1 -1
- package/dist/builders/modifier.js +14 -0
- package/dist/builders/object/guard.d.ts +54 -2
- package/dist/builders/object/guard.d.ts.map +1 -1
- package/dist/builders/object/guard.js +117 -7
- package/dist/builders/object/index.d.ts +2 -2
- package/dist/builders/object/index.d.ts.map +1 -1
- package/dist/builders/object/index.js +1 -1
- package/dist/builders/object/schema.d.ts +33 -2
- package/dist/builders/object/schema.d.ts.map +1 -1
- package/dist/builders/object/schema.js +198 -8
- package/dist/builders/object/types.d.ts +15 -0
- package/dist/builders/object/types.d.ts.map +1 -1
- package/dist/builders/runtime.d.ts +40 -0
- package/dist/builders/runtime.d.ts.map +1 -0
- package/dist/builders/runtime.js +150 -0
- package/dist/builders/scalar.d.ts +20 -1
- package/dist/builders/scalar.d.ts.map +1 -1
- package/dist/builders/scalar.js +54 -1
- package/dist/builders/table.d.ts +31 -5
- package/dist/builders/table.d.ts.map +1 -1
- package/dist/builders/table.js +31 -5
- package/dist/builders/types.d.ts +6 -0
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/compile/check-composite.d.ts +3 -1
- package/dist/compile/check-composite.d.ts.map +1 -1
- package/dist/compile/check-composite.js +143 -11
- package/dist/compile/check-scalar.d.ts +10 -0
- package/dist/compile/check-scalar.d.ts.map +1 -1
- package/dist/compile/check-scalar.js +138 -2
- package/dist/compile/check.d.ts.map +1 -1
- package/dist/compile/check.js +25 -3
- package/dist/compile/context.d.ts.map +1 -1
- package/dist/compile/context.js +2 -0
- package/dist/compile/first.d.ts +26 -0
- package/dist/compile/first.d.ts.map +1 -0
- package/dist/compile/first.js +850 -0
- package/dist/compile/graph-predicate.d.ts.map +1 -1
- package/dist/compile/graph-predicate.js +389 -18
- package/dist/compile/guard.d.ts +2 -1
- package/dist/compile/guard.d.ts.map +1 -1
- package/dist/compile/guard.js +54 -8
- package/dist/compile/predicate.d.ts.map +1 -1
- package/dist/compile/predicate.js +156 -5
- package/dist/compile/runtime.d.ts +20 -1
- package/dist/compile/runtime.d.ts.map +1 -1
- package/dist/compile/runtime.js +31 -0
- package/dist/compile/source.d.ts.map +1 -1
- package/dist/compile/source.js +27 -3
- package/dist/compile/types.d.ts +2 -0
- package/dist/compile/types.d.ts.map +1 -1
- package/dist/decoder/index.d.ts +60 -0
- package/dist/decoder/index.d.ts.map +1 -1
- package/dist/decoder/index.js +164 -1
- package/dist/evaluate/check-composite.d.ts +52 -2
- package/dist/evaluate/check-composite.d.ts.map +1 -1
- package/dist/evaluate/check-composite.js +193 -6
- package/dist/evaluate/check-scalar.d.ts +9 -0
- package/dist/evaluate/check-scalar.d.ts.map +1 -1
- package/dist/evaluate/check-scalar.js +92 -3
- package/dist/evaluate/check.d.ts.map +1 -1
- package/dist/evaluate/check.js +19 -4
- package/dist/evaluate/shared.d.ts +19 -0
- package/dist/evaluate/shared.d.ts.map +1 -1
- package/dist/evaluate/shared.js +35 -0
- package/dist/guard/array.d.ts +48 -0
- package/dist/guard/array.d.ts.map +1 -0
- package/dist/guard/array.js +84 -0
- package/dist/guard/base.d.ts +32 -2
- package/dist/guard/base.d.ts.map +1 -1
- package/dist/guard/base.js +74 -3
- package/dist/guard/date.d.ts +34 -0
- package/dist/guard/date.d.ts.map +1 -0
- package/dist/guard/date.js +60 -0
- package/dist/guard/index.d.ts +2 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +2 -0
- package/dist/guard/number.d.ts +60 -0
- package/dist/guard/number.d.ts.map +1 -1
- package/dist/guard/number.js +129 -0
- package/dist/guard/read.d.ts +53 -1
- package/dist/guard/read.d.ts.map +1 -1
- package/dist/guard/read.js +102 -0
- package/dist/guard/string.d.ts +82 -0
- package/dist/guard/string.d.ts.map +1 -1
- package/dist/guard/string.js +213 -0
- package/dist/guard/types.d.ts +18 -0
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/ir/builder.d.ts +3 -3
- package/dist/ir/builder.d.ts.map +1 -1
- package/dist/ir/builder.js +5 -2
- package/dist/ir/freeze.js +7 -0
- package/dist/ir/types.d.ts +4 -1
- package/dist/ir/types.d.ts.map +1 -1
- package/dist/ir/validate.d.ts.map +1 -1
- package/dist/ir/validate.js +35 -1
- package/dist/issue/index.d.ts +1 -1
- package/dist/issue/index.d.ts.map +1 -1
- package/dist/issue/index.js +4 -0
- package/dist/json-schema/emit-composite.d.ts +6 -2
- package/dist/json-schema/emit-composite.d.ts.map +1 -1
- package/dist/json-schema/emit-composite.js +66 -12
- package/dist/json-schema/emit-scalar.d.ts.map +1 -1
- package/dist/json-schema/emit-scalar.js +54 -1
- package/dist/json-schema/emit.d.ts.map +1 -1
- package/dist/json-schema/emit.js +11 -2
- package/dist/json-schema/types.d.ts +7 -1
- package/dist/json-schema/types.d.ts.map +1 -1
- package/dist/kind/index.d.ts +25 -0
- package/dist/kind/index.d.ts.map +1 -1
- package/dist/kind/index.js +26 -3
- package/dist/lower/index.d.ts.map +1 -1
- package/dist/lower/index.js +52 -3
- package/dist/message/index.d.ts +18 -0
- package/dist/message/index.d.ts.map +1 -1
- package/dist/message/index.js +67 -0
- package/dist/optimize/domain.js +6 -2
- package/dist/optimize/map-node.d.ts.map +1 -1
- package/dist/optimize/map-node.js +3 -0
- package/dist/plan/cache.js +13 -1
- package/dist/plan/predicate.d.ts.map +1 -1
- package/dist/plan/predicate.js +33 -3
- package/dist/plan/schema-predicate.d.ts.map +1 -1
- package/dist/plan/schema-predicate.js +267 -8
- package/dist/schema/freeze.js +22 -0
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/types.d.ts +89 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +8 -1
- package/dist/schema/undefined.d.ts.map +1 -1
- package/dist/schema/undefined.js +5 -0
- package/dist/schema/validate.d.ts.map +1 -1
- package/dist/schema/validate.js +111 -4
- package/docs/api.md +79 -10
- package/docs/assets/benchmark-headline.svg +33 -33
- package/docs/engine-notes.md +9 -5
- package/docs/index.html +1366 -722
- package/docs/ko/api.md +383 -0
- package/docs/ko/engine-notes.md +156 -0
- package/docs/ko/readme.md +404 -0
- package/package.json +6 -2
package/docs/ko/api.md
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# TypeSea API 레퍼런스
|
|
2
|
+
|
|
3
|
+
TypeSea는 신뢰할 수 없는 값을 `unknown`으로 받고, 불변 guard를 통해 타입을 좁힙니다.
|
|
4
|
+
공개 API는 작게 유지하고, 복잡한 검증 로직은 builder validation, graph introspection, diagnostics, export check 내부에 둡니다.
|
|
5
|
+
|
|
6
|
+
## 가져오기
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import {
|
|
10
|
+
compile,
|
|
11
|
+
emitAotModule,
|
|
12
|
+
t,
|
|
13
|
+
toJsonSchema,
|
|
14
|
+
type Guard,
|
|
15
|
+
type Infer
|
|
16
|
+
} from "typesea";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
패키지는 root entry point 하나만 노출합니다.
|
|
20
|
+
subpath import는 공개 API가 아닙니다.
|
|
21
|
+
TypeSea는 ESM-only이며 CommonJS condition을 publish하지 않습니다.
|
|
22
|
+
|
|
23
|
+
## Guard 계약
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
interface Guard<T> {
|
|
27
|
+
is(value: unknown): value is T;
|
|
28
|
+
check(value: unknown): CheckResult<T>;
|
|
29
|
+
checkFirst(value: unknown): CheckResult<T>;
|
|
30
|
+
assert(value: unknown): asserts value is T;
|
|
31
|
+
graph(): Graph;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| 메서드 | 용도 | 계약 |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `is` | 빠른 boolean narrowing | 성공 경로에서 진단 객체를 만들지 않습니다. |
|
|
38
|
+
| `check` | 실패 이유가 필요한 검증 | 동결된 `Result<T, Issue[]>` container를 반환합니다. |
|
|
39
|
+
| `checkFirst` | hot path의 단일 실패 진단 | 같은 `Result` 형태를 반환하되 실패 시 issue를 최대 하나만 담습니다. compiled/AOT guard는 전용 first-fault collector를 사용합니다. |
|
|
40
|
+
| `assert` | 예외가 필요한 연동 지점 | 복사되고 동결된 issue를 담은 `TypeSeaAssertionError`를 던집니다. |
|
|
41
|
+
| `graph` | 검증 계획 introspection | validation plan이 보유한 validated, optimized, frozen Sea-of-Nodes graph를 반환합니다. |
|
|
42
|
+
|
|
43
|
+
diagnostic path에는 object key와 0부터 시작하는 array 또는 tuple index만 들어갑니다.
|
|
44
|
+
공개 diagnostic validator는 잘못된 path segment를 거부한 뒤 diagnostic을 API 밖으로 내보냅니다.
|
|
45
|
+
|
|
46
|
+
## Builder 계열
|
|
47
|
+
|
|
48
|
+
| 계열 | Builder |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| Scalar | `t.unknown`, `t.never`, `t.string`, `t.number`, `t.date`, `t.bigint`, `t.symbol`, `t.boolean`, `t.null`, `t.undefined`, `t.void` |
|
|
51
|
+
| String check | `.min`, `.max`, `.length`, `.nonempty`, `.regex`, `.startsWith`, `.endsWith`, `.includes`, `.uuid`, `.email`, `.url`, `.isoDate`, `.isoDateTime`, `.ulid`, `.ipv4`, `.ipv6` |
|
|
52
|
+
| Number check | `.int`, `.finite`, `.safe`, `.gte`, `.lte`, `.min`, `.max`, `.gt`, `.lt`, `.multipleOf`, `.positive`, `.nonnegative`, `.negative`, `.nonpositive` |
|
|
53
|
+
| Date check | `.min`, `.max` |
|
|
54
|
+
| Literal과 container | `t.literal(value)`, `t.enum(values)`, `t.array(item)`, `t.tuple([a, b])`, `t.tuple([head], rest)`, `t.record(value)`, `t.map(key, value)`, `t.set(item)`, `t.json()` |
|
|
55
|
+
| Array check | `.min`, `.max`, `.length`, `.nonempty` |
|
|
56
|
+
| Object | `t.object(shape)`, `t.strictObject(shape)` |
|
|
57
|
+
| Object transform | `t.extend`, `t.safeExtend`, `t.merge`, `t.pick`, `t.omit`, `t.partial`, `t.deepPartial`, `t.required`, `t.strict`, `t.passthrough`, `t.strip`, `t.catchall`, 그리고 같은 이름의 object guard method |
|
|
58
|
+
| Runtime object contract | `t.instanceOf(Ctor)`, `t.property(base, key, value)`, `guard.property(key, value)` |
|
|
59
|
+
| Composition | `t.union`, `t.discriminatedUnion`, `t.intersect`, `guard.intersect` |
|
|
60
|
+
| Presence | `t.optional`, `t.undefinedable`, `t.nullable`, `t.nullish` |
|
|
61
|
+
| Dynamic guard | `t.lazy`, `t.refine` |
|
|
62
|
+
| Decoder | `t.decoder`, `t.transform`, `t.pipe`, `t.default`, `t.defaultValue`, `t.prefault`, `t.catch`, `t.codec`, `t.coerce`, `t.string.trim()`, `t.string.toLowerCase()`, `t.string.toUpperCase()` |
|
|
63
|
+
| Async decoder | `t.asyncDecoder`, `t.asyncRefine`, `t.asyncTransform`, `t.asyncPipe` |
|
|
64
|
+
|
|
65
|
+
builder function은 schema가 validation plan, compiler, AOT emitter, diagnostic collector, JSON Schema exporter로 들어가기 전에 입력을 검증합니다.
|
|
66
|
+
위조된 guard-like value, 잘못된 schema tag, 잘못된 predicate, 잘못된 bound, malformed regexp, 잘못된 discriminated union case set은 construction 중 거부됩니다.
|
|
67
|
+
|
|
68
|
+
허용된 schema는 저장 전에 freeze됩니다.
|
|
69
|
+
공개 schema collection field는 변경 가능한 collection object 대신 frozen array와 frozen key lookup record를 사용합니다.
|
|
70
|
+
|
|
71
|
+
## 객체 key 존재 규칙
|
|
72
|
+
|
|
73
|
+
TypeSea는 key가 존재하는지와 value domain을 분리합니다.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const Shape = t.object({
|
|
77
|
+
name: t.optional(t.string),
|
|
78
|
+
nickname: t.undefinedable(t.string)
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `name`은 없어도 됩니다. 존재한다면 값은 string이어야 합니다.
|
|
83
|
+
- `nickname`은 반드시 존재해야 합니다. 값은 string 또는 `undefined`일 수 있습니다.
|
|
84
|
+
- `t.nullable(inner)`는 value domain에 `null`을 추가합니다.
|
|
85
|
+
- `t.nullish(inner)`는 nullable value와 optional key 의미를 함께 제공합니다.
|
|
86
|
+
- `nullable`, `undefinedable`, `brand`, `refine`을 지나도 optional-key 의미는 보존됩니다.
|
|
87
|
+
|
|
88
|
+
object combinator는 object mode를 보존합니다.
|
|
89
|
+
strict object guard는 `extend`, `pick`, `omit`, `partial` 이후에도 strict를 유지하고, passthrough object guard는 unknown key 허용을 유지합니다.
|
|
90
|
+
|
|
91
|
+
`catchall(schema)`는 선언되지 않은 모든 own key를 `schema`로 검증합니다.
|
|
92
|
+
`strip()`은 TypeSea에서 검증 전용 의미입니다. guard는 원본 값을 반환하므로, 검증 의미는 `passthrough()`와 같습니다.
|
|
93
|
+
`pick`과 `omit`은 key array와 Zod 스타일 `{ key: true }` mask를 모두 받습니다.
|
|
94
|
+
`deepPartial()`은 순수 object, array, tuple, tuple rest, record, map, set, property, union, intersection, nullable, undefinedable, optional, brand schema를 재귀적으로 partial 처리합니다.
|
|
95
|
+
lazy와 refinement schema는 callback 의미를 보존하기 위해 semantic barrier로 둡니다.
|
|
96
|
+
|
|
97
|
+
`property`는 own data descriptor만 검증합니다. 안정적인 class field를 증명할 때 쓰기 좋고, prototype getter나 accessor property는 실행하지 않고 거부합니다.
|
|
98
|
+
|
|
99
|
+
## 합성
|
|
100
|
+
|
|
101
|
+
`t.union(a, b)`는 적어도 한 branch를 만족하는 값을 허용합니다.
|
|
102
|
+
|
|
103
|
+
`t.discriminatedUnion("kind", cases)`는 string case key를 요구합니다.
|
|
104
|
+
각 case는 static하게 inspect할 수 있는 object case여야 하며, dispatch key는 case name과 일치하는 required string literal이어야 합니다.
|
|
105
|
+
|
|
106
|
+
`t.intersect(a, b)`와 `guard.intersect(other)`는 같은 input value가 두 guard를 모두 만족해야 합니다.
|
|
107
|
+
`check()`는 양쪽 diagnostic을 모두 수집합니다.
|
|
108
|
+
|
|
109
|
+
## 재귀
|
|
110
|
+
|
|
111
|
+
recursive contract는 반드시 `t.lazy`를 사용해야 합니다.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
interface ListNode {
|
|
115
|
+
readonly value: string;
|
|
116
|
+
readonly next?: ListNode;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const Node: Guard<ListNode> = t.lazy((): Guard<ListNode> =>
|
|
120
|
+
t.object({
|
|
121
|
+
value: t.string,
|
|
122
|
+
next: t.optional(Node)
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
직접 순환하는 schema object는 builder boundary에서 거부됩니다.
|
|
128
|
+
lazy guard는 guard instance마다 한 번 resolve되고 recursive schema identity를 안정적으로 유지합니다.
|
|
129
|
+
lazy chain은 결국 concrete non-lazy schema로 resolve되어야 합니다.
|
|
130
|
+
|
|
131
|
+
## Decoder Pipeline
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const Count = t.pipe(t.coerce.number(), t.number.int().gte(0));
|
|
135
|
+
const result = Count.decode("42");
|
|
136
|
+
|
|
137
|
+
const Name = t.default(t.string.min(1), "anonymous");
|
|
138
|
+
const NormalizedName = t.string
|
|
139
|
+
.trim()
|
|
140
|
+
.pipe(t.string.min(1))
|
|
141
|
+
.transform((value) => value.toLowerCase())
|
|
142
|
+
.default("anonymous")
|
|
143
|
+
.catch("anonymous");
|
|
144
|
+
const NumberText = t.codec(
|
|
145
|
+
t.string.regex(/^\d+$/u, "digits"),
|
|
146
|
+
t.number.int().nonnegative(),
|
|
147
|
+
{
|
|
148
|
+
decode: (value) => Number(value),
|
|
149
|
+
encode: (value) => String(value)
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
decoder는 output을 생성하는 작업에 씁니다.
|
|
155
|
+
`decode()`에서 `Result`를 반환하며 `is()` predicate를 노출하지 않습니다.
|
|
156
|
+
decoded output이 input과 같은 runtime value가 아닐 수 있기 때문입니다.
|
|
157
|
+
|
|
158
|
+
- `t.transform(source, mapper)`는 `source`를 decode한 뒤 decoded value를 map합니다.
|
|
159
|
+
- `t.pipe(source, next)`는 성공한 decoded value를 다음 guard 또는 decoder에 넘깁니다.
|
|
160
|
+
- `t.default(source, value)`는 input이 `undefined`일 때 fallback output을 바로 반환합니다.
|
|
161
|
+
- `t.prefault(source, value)`는 input이 `undefined`일 때 fallback input을 source에 다시 통과시킵니다.
|
|
162
|
+
- `t.catch(source, value)`는 decode 실패 시 fallback output을 반환합니다.
|
|
163
|
+
- `t.codec(input, output, mapping)`은 bidirectional decode/encode 양쪽을 모두 검증합니다.
|
|
164
|
+
- `t.coerce.string`, `t.coerce.number`, `t.coerce.boolean`은 명시적 primitive coercion을 제공합니다.
|
|
165
|
+
- `t.string.trim()`, `t.string.toLowerCase()`, `t.string.toUpperCase()`는 decoder helper입니다. 먼저 string을 검증한 뒤 `decode()` 결과로 변환된 값을 반환합니다.
|
|
166
|
+
- `t.asyncRefine`, `t.asyncTransform`, `t.asyncPipe`는 `decodeAsync()`에서 `Promise<Result<T, Issue[]>>`를 반환합니다.
|
|
167
|
+
|
|
168
|
+
예상 가능한 async validation 실패도 `Result`로 반환됩니다.
|
|
169
|
+
|
|
170
|
+
## Message
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const checked = withMessages(User.check(input), {
|
|
174
|
+
locale: "ko",
|
|
175
|
+
catalog: defineMessages({
|
|
176
|
+
expected_string: "{path}: 문자열 필요"
|
|
177
|
+
})
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`formatIssue`, `formatIssues`, `flattenIssues`, `withMessages`는 validation이 끝난 뒤 diagnostic을 렌더링합니다.
|
|
182
|
+
따라서 `is()`와 일반 `check()` path에서는 message allocation이 발생하지 않습니다.
|
|
183
|
+
|
|
184
|
+
built-in locale은 `en`과 `ko`입니다.
|
|
185
|
+
custom catalog는 `{path}`, `{code}`, `{expected}`, `{actual}` string template 또는 formatter callback을 쓸 수 있습니다.
|
|
186
|
+
`withMessages(result, options)`는 successful result를 그대로 보존하고, failed `Result`에는 복사되고 동결된 issue에 `message` field를 채워 새로 반환합니다.
|
|
187
|
+
`flattenIssues(issues, options)`는 렌더링된 message를 `formErrors`와 top-level `fieldErrors` bucket으로 묶습니다.
|
|
188
|
+
|
|
189
|
+
## 런타임 컴파일
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const FastUser = compile(User, { name: "isUser" });
|
|
193
|
+
|
|
194
|
+
FastUser.is(input);
|
|
195
|
+
FastUser.check(input);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`compile`은 optimized Sea-of-Nodes validation graph에서 generated predicate function과 failed value용 diagnostic collector를 방출합니다.
|
|
199
|
+
static scalar, object, array, record, union, strict-key node는 가능한 경우 straight-line JavaScript 또는 indexed loop로 낮아집니다.
|
|
200
|
+
`lazy`, `refine` 같은 dynamic schema edge는 ordinary guard execution과 같은 IR-backed runtime fallback을 사용해 의미를 유지합니다.
|
|
201
|
+
|
|
202
|
+
선택적 `name`은 debugging과 profiling을 위한 hint입니다.
|
|
203
|
+
TypeSea는 이를 strict-mode-safe JavaScript function name으로 normalize하고, reserved name에는 prefix를 붙이며, generated name 길이에 cap을 둡니다.
|
|
204
|
+
직접 compiled guard construction은 predicate, collector, source argument를 검증합니다.
|
|
205
|
+
collector diagnostic은 `check()` 반환 전에 validate, copy, freeze됩니다.
|
|
206
|
+
|
|
207
|
+
generated source는 사용자가 제어하는 값을 직접 interpolate하지 않습니다.
|
|
208
|
+
literal, regexp, property key, keyset, dynamic schema fallback은 side table에 capture되고 numeric index로 참조됩니다.
|
|
209
|
+
|
|
210
|
+
### Unsafe 컴파일 모드
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const FastButLooseUser = compile(User, {
|
|
214
|
+
name: "isUserFast",
|
|
215
|
+
mode: "unsafe"
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`CompileOptions["mode"]`와 `AotCompileOptions["mode"]`는 `"safe" | "unsafe" | "unchecked" | undefined`입니다.
|
|
220
|
+
option을 생략하면 `"safe"`가 기본입니다.
|
|
221
|
+
safe mode는 TypeSea의 적대적 입력 방어 계약을 유지합니다.
|
|
222
|
+
descriptor 기반 property read, getter 실행 금지, symbol과 non-enumerable extra를 포함한 strict-object rejection을 보장합니다.
|
|
223
|
+
|
|
224
|
+
unsafe mode는 신뢰할 수 있고 정규화된 plain data를 위한 명시적 performance escape hatch입니다.
|
|
225
|
+
|
|
226
|
+
- field schema가 `undefined`를 거부하는 required object field는 `value[key]`로 읽습니다.
|
|
227
|
+
- discriminant dispatch는 tag를 direct bracket access로 읽습니다.
|
|
228
|
+
- array와 tuple은 direct indexed load를 사용합니다.
|
|
229
|
+
- strict-object extra-key rejection은 allocation-free own-enumerable `for...in` loop를 사용합니다.
|
|
230
|
+
|
|
231
|
+
이 모드는 getter를 실행할 수 있고, prototype-backed value를 받아들일 수 있으며, strict object에서 symbol 또는 non-enumerable extra를 거부하지 않습니다.
|
|
232
|
+
compiled `check()`는 먼저 generated predicate의 판정을 신뢰하므로, unsafe predicate가 `true`를 반환하면 `check()`도 successful result를 반환합니다.
|
|
233
|
+
input이 trusted normalization boundary를 지난 뒤에만 unsafe mode를 사용하세요.
|
|
234
|
+
|
|
235
|
+
unsafe mode는 escaped static property key를 generated predicate source에 직접 넣을 수 있습니다.
|
|
236
|
+
그래야 V8이 ordinary property-load inline cache를 붙이기 쉽습니다.
|
|
237
|
+
safe mode는 property key를 side table에 유지합니다.
|
|
238
|
+
|
|
239
|
+
unchecked mode는 unsafe direct-read shape을 사용하고 strict-object extra-key loop도 건너뜁니다.
|
|
240
|
+
object shape이 이미 신뢰되거나 정규화된 input에만 사용해야 합니다.
|
|
241
|
+
이 모드에서는 strict object가 더 이상 extra key를 거부하지 않습니다.
|
|
242
|
+
|
|
243
|
+
unsafe와 unchecked compiled `check()`는 successful Result object를 `Object.freeze()` 없이 raw object로 반환합니다.
|
|
244
|
+
failure diagnostic은 계속 freeze됩니다.
|
|
245
|
+
safe mode는 success와 failure 모두 frozen Result object를 유지합니다.
|
|
246
|
+
FastMode diagnostic collector는 가능한 경우 direct field read와 FastMode strict-key rule을 사용합니다.
|
|
247
|
+
따라서 missing/accessor issue code는 safe mode와 일치한다고 보장하지 않습니다.
|
|
248
|
+
array와 tuple diagnostic도 fast mode에서는 direct indexed read를 쓰므로 sparse slot은 loaded `undefined` value 기준으로 진단됩니다.
|
|
249
|
+
record diagnostic은 direct `record[key]` read를 사용합니다.
|
|
250
|
+
unchecked mode는 inherited enumerable key도 방문합니다.
|
|
251
|
+
discriminant diagnostic은 tag를 직접 읽고 string case를 `===`로 비교합니다.
|
|
252
|
+
|
|
253
|
+
## AOT 모듈 생성
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const emitted = emitAotModule(User, { name: "aotUser" });
|
|
257
|
+
const unsafeEmitted = emitAotModule(User, {
|
|
258
|
+
name: "aotUserFast",
|
|
259
|
+
mode: "unsafe"
|
|
260
|
+
});
|
|
261
|
+
const uncheckedEmitted = emitAotModule(User, {
|
|
262
|
+
name: "aotUserTrustedShape",
|
|
263
|
+
mode: "unchecked"
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`emitAotModule`은 `Result<AotModule, AotIssue[]>`를 반환합니다.
|
|
268
|
+
successful result에는 standalone ESM validator source와 declaration source가 들어 있습니다.
|
|
269
|
+
generated module은 module load time에 dynamic source compilation을 요구하지 않고 `is`, `check`, `assert`, default frozen guard-like object를 export합니다.
|
|
270
|
+
|
|
271
|
+
AOT generation은 lossless-only입니다.
|
|
272
|
+
runtime callback 또는 serialize할 수 없는 identity가 필요한 schema는 명시적 AOT issue를 반환합니다.
|
|
273
|
+
|
|
274
|
+
## Framework Adapter
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const parser = toTrpcParser(User);
|
|
278
|
+
const routeSchema = toFastifyRouteSchema(User);
|
|
279
|
+
const validatorCompiler = toFastifyValidatorCompiler(User);
|
|
280
|
+
const resolver = toReactHookFormResolver(User);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
adapter는 구조적으로 맞춰진 얇은 연결 계층이며 런타임 의존성이 없습니다.
|
|
284
|
+
TypeSea는 tRPC, Fastify, React Hook Form을 import하지 않습니다.
|
|
285
|
+
|
|
286
|
+
compiled guard도 같은 adapter에 넘길 수 있습니다.
|
|
287
|
+
hot request path에서는 이 형태를 권장합니다.
|
|
288
|
+
startup에서 한 번 compile한 뒤 adapter가 generated predicate를 재사용하게 하세요.
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
const FastUser = compile(User);
|
|
292
|
+
const fastParser = toTrpcParser(FastUser);
|
|
293
|
+
const fastValidatorCompiler = toFastifyValidatorCompiler(FastUser);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
public input boundary에서는 기본 compiled mode를 쓰세요.
|
|
297
|
+
adapter가 직접 `is()` 호출을 숨기더라도 safe descriptor-read 계약은 유지됩니다.
|
|
298
|
+
신뢰된, 이미 정규화된 내부 데이터에서는 더 빠른 mode를 같은 방식으로 adapter에 연결할 수 있습니다.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const UnsafeUser = compile(User, { mode: "unsafe" });
|
|
302
|
+
const internalParser = toTrpcParser(UnsafeUser);
|
|
303
|
+
|
|
304
|
+
const TrustedShapeUser = compile(User, { mode: "unchecked" });
|
|
305
|
+
const internalValidatorCompiler = toFastifyValidatorCompiler(TrustedShapeUser);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
| Adapter | Export | 동작 |
|
|
309
|
+
| --- | --- | --- |
|
|
310
|
+
| tRPC | `toTrpcParser`, `toAsyncTrpcParser` | decoded value를 반환하거나 `TypeSeaAssertionError`를 던지는 parser object를 반환합니다. |
|
|
311
|
+
| Fastify route schema | `toFastifyRouteSchema` | guard를 JSON Schema route fragment로 변환합니다. |
|
|
312
|
+
| Fastify validator compiler | `toFastifyValidatorCompiler` | `{ value }` 또는 `{ error }`를 만드는 compiler-shaped validator를 반환합니다. |
|
|
313
|
+
| React Hook Form | `toReactHookFormResolver` | TypeSea message를 field error로 매핑하는 async resolver를 반환합니다. |
|
|
314
|
+
|
|
315
|
+
## Graph and IR
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
const graph = User.graph();
|
|
319
|
+
const optimized = optimizeGraph(graph);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`Guard.graph()`는 runtime validation plan이 보유한 optimized Sea-of-Nodes validation graph를 반환합니다.
|
|
323
|
+
같은 plan은 `is()`가 사용하는 specialized predicate kernel도 소유합니다.
|
|
324
|
+
graph는 `compile()`과 `emitAotModule()`의 source이고, kernel은 ordinary guard execution이 generic per-node interpreter를 타지 않게 합니다.
|
|
325
|
+
공개 graph value는 validate, dependency-check, dense compaction, freeze를 거쳐 반환됩니다.
|
|
326
|
+
|
|
327
|
+
`optimizeGraph(graph)`는 직접 전달된 graph input을 validate한 뒤 optimize합니다.
|
|
328
|
+
regex graph node는 plain `RegExp` value만 받으며, graph가 freeze되기 전에 extensible input을 clone해서 non-extensible regexp로 저장합니다.
|
|
329
|
+
|
|
330
|
+
`SchemaCheck`는 `lazy`나 `refine`처럼 dynamic runtime schema logic을 기록합니다.
|
|
331
|
+
callback-backed edge를 static primitive인 척하지 않고, runtime semantics가 필요하다는 사실을 IR에 정확히 남깁니다.
|
|
332
|
+
|
|
333
|
+
## JSON Schema 내보내기
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
const result = toJsonSchema(User);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`toJsonSchema`는 `Result<JsonSchema, JsonSchemaExportIssue[]>`를 반환합니다.
|
|
340
|
+
TypeSea가 JSON-compatible input value 위에서 contract를 의미 손실 없이 표현할 수 있을 때만 성공합니다.
|
|
341
|
+
|
|
342
|
+
runtime-only concept는 명시적 export issue를 반환합니다.
|
|
343
|
+
|
|
344
|
+
- `undefined`
|
|
345
|
+
- `bigint`
|
|
346
|
+
- `symbol`
|
|
347
|
+
- JavaScript `Date`, `Map`, `Set`, `instanceOf`, `property` contract
|
|
348
|
+
- `lazy`
|
|
349
|
+
- `refine`
|
|
350
|
+
- decoder transforms
|
|
351
|
+
- async validation
|
|
352
|
+
- flag가 있는 regexp
|
|
353
|
+
- `NaN`, `Infinity`, `-0`처럼 JSON이 보존할 수 없는 numeric literal
|
|
354
|
+
|
|
355
|
+
`schemaToJsonSchema(schema)`는 direct schema API입니다.
|
|
356
|
+
전달된 schema를 validate하고 freeze한 뒤 export합니다.
|
|
357
|
+
JSON Schema option도 validate합니다.
|
|
358
|
+
`schemaId`가 있으면 string이어야 합니다.
|
|
359
|
+
|
|
360
|
+
object `properties` map은 null-prototype record로 방출됩니다.
|
|
361
|
+
따라서 `__proto__`, `constructor`, `hasOwnProperty` 같은 특수 key도 ordinary own schema property로 남습니다.
|
|
362
|
+
|
|
363
|
+
## 경계 동작
|
|
364
|
+
|
|
365
|
+
- literal guard는 `Object.is`를 사용합니다. 따라서 `t.literal(Number.NaN)`은 `NaN`을 match하고 `t.literal(-0)`은 `0`과 match하지 않습니다.
|
|
366
|
+
- `t.number`는 finite JavaScript number만 허용합니다. `NaN`, `Infinity`, `-Infinity`는 configured numeric predicate가 실행되기 전에 거부됩니다.
|
|
367
|
+
- string length bound는 non-negative integer여야 합니다.
|
|
368
|
+
- numeric comparison bound는 finite number여야 합니다.
|
|
369
|
+
- predicate callback은 strict `true`를 반환해야 합니다. truthy non-boolean value는 validation을 통과하지 않습니다.
|
|
370
|
+
- `RegExp` check는 매 test 전에 `lastIndex`를 reset합니다. global과 sticky regexp의 상태가 validation 사이에 새지 않습니다.
|
|
371
|
+
- string regex builder와 direct string regex schema는 plain `RegExp` instance만 받습니다. 허용된 regex check는 storage 전에 clone됩니다.
|
|
372
|
+
|
|
373
|
+
## Result 계약
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
type Result<T, E> =
|
|
377
|
+
| { ok: true; value: T }
|
|
378
|
+
| { ok: false; error: E };
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
예상 가능한 validation failure는 `Result`를 사용합니다.
|
|
382
|
+
Result container는 runtime에서 freeze됩니다.
|
|
383
|
+
successful value는 caller-owned data이므로 recursive freeze하지 않습니다.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# 엔진 설계 노트
|
|
2
|
+
|
|
3
|
+
TypeSea는 TypeScript가 JavaScript를 emit한 뒤의 실행 특성을 예측 가능하게 만들기 위해 작성했습니다.
|
|
4
|
+
목표는 난해한 코드가 아닙니다.
|
|
5
|
+
object shape, allocation site, branch behavior, validation contract가 코드에서 드러나도록 만드는 것이 목표입니다.
|
|
6
|
+
|
|
7
|
+
## Hot Path 규칙
|
|
8
|
+
|
|
9
|
+
- per-instance method closure 대신 prototype method를 사용합니다.
|
|
10
|
+
- schema, check, issue, IR node variant에는 numeric tag를 씁니다.
|
|
11
|
+
- class field는 모든 constructor에서 같은 순서로 초기화합니다.
|
|
12
|
+
- successful `is()` validation은 diagnostic allocation을 만들지 않습니다.
|
|
13
|
+
- `Issue` object와 path array는 diagnostic이 요청될 때만 할당합니다.
|
|
14
|
+
- recursive validation path에서는 indexed loop를 선호합니다.
|
|
15
|
+
- object-entry array는 schema construction 중 미리 계산합니다.
|
|
16
|
+
- required object field가 data-property descriptor임을 증명한 뒤에는 missing-property fallback을 다시 검사하지 않고 descriptor value를 직접 읽습니다.
|
|
17
|
+
- 모든 field가 required인 strict object는 field validation 뒤 own string name과 own symbol 수를 세어 extra를 거부합니다. optional strict object는 전체 key membership scan을 유지합니다.
|
|
18
|
+
- `compile()`과 `emitAotModule()`은 safe가 기본입니다. unsafe mode는 명시적 opt-in이며, caller가 getter, prototype, symbol-extra risk를 받아들였을 때 direct property/index load와 own-enumerable strict-key loop를 쓸 수 있습니다.
|
|
19
|
+
- constructed guard는 out-of-band로 표시합니다. 정상 receiver는 반복 schema validation을 피하고, forged receiver는 structural check로 fallback합니다.
|
|
20
|
+
- object guard 뒤에는 `Readonly<Record<string, unknown>>`을 사용합니다.
|
|
21
|
+
- generated-validator literal, regexp, keyset, dynamic fallback은 사용자가 제어하는 값을 source text에 interpolate하지 않고 side table에 저장합니다.
|
|
22
|
+
|
|
23
|
+
## 타입 시스템 규칙
|
|
24
|
+
|
|
25
|
+
- `optional(inner)`은 object key가 없어도 된다는 뜻입니다.
|
|
26
|
+
- `undefinedable(inner)`은 object shape에서 key가 반드시 존재하지만 value가 `undefined`일 수 있다는 뜻입니다.
|
|
27
|
+
- `nullable(inner)`은 value가 `null`일 수 있다는 뜻입니다.
|
|
28
|
+
- presence-preserving wrapper는 optional-key semantics를 지우지 않습니다.
|
|
29
|
+
- `number`는 finite JavaScript number를 의미합니다.
|
|
30
|
+
- 신뢰할 수 없는 입력의 boundary type은 `unknown`만 허용합니다.
|
|
31
|
+
- builder validation은 schema가 engine에 도달하기 전의 hard barrier입니다.
|
|
32
|
+
|
|
33
|
+
## IR 규칙
|
|
34
|
+
|
|
35
|
+
public schema tree는 builder와 diagnostic collector가 사용하는 semantic source입니다.
|
|
36
|
+
boolean validation은 cached `ValidationPlan`을 실행합니다.
|
|
37
|
+
schema identity는 Sea-of-Nodes IR로 낮아지고 optimizer를 거치며, plan은 frozen graph와 schema-specialized predicate kernel을 함께 보유합니다.
|
|
38
|
+
|
|
39
|
+
graph는 장식이 아닙니다.
|
|
40
|
+
`compile()`, AOT emission, `Guard.graph()`는 모두 plan이 보유한 optimized graph를 소비합니다.
|
|
41
|
+
ordinary `Guard.is()`는 generic node interpreter 대신 sibling schema-specialized kernel을 의도적으로 사용합니다.
|
|
42
|
+
가장 흔한 hot path에서는 per-node dispatch와 scratch-slot bookkeeping 비용이 이득보다 크기 때문입니다.
|
|
43
|
+
|
|
44
|
+
현재 lowering은 pure value node와 predicate node를 hash-cons합니다.
|
|
45
|
+
strict object schema는 explicit keyset check를 IR로 낮춥니다.
|
|
46
|
+
따라서 extra-key rejection이 out-of-band schema knowledge에 의존하지 않습니다.
|
|
47
|
+
required object field와 optional object field는 key presence와 data-property presence를 분리합니다.
|
|
48
|
+
이 방식은 accessor-backed property의 getter를 실행하거나 valid value로 잘못 분류하지 않게 합니다.
|
|
49
|
+
|
|
50
|
+
`Guard.graph()`는 validation plan이 보유한 동일한 optimized graph를 반환합니다.
|
|
51
|
+
public graph value는 API 밖으로 나가기 전에 validate되고 freeze됩니다.
|
|
52
|
+
첫 optimizer pass는 reachable node elimination을 수행하고 node id를 compact해서 모든 dependency가 존재하는 dense node index를 가리키게 합니다.
|
|
53
|
+
`compile()`과 AOT emission은 이 graph를 predicate source로 사용합니다.
|
|
54
|
+
|
|
55
|
+
array, tuple, record schema는 native composite IR node로 낮아지며, child schema는 child validation plan을 통해 실행됩니다.
|
|
56
|
+
`SchemaCheck`는 `lazy`와 `refine` 같은 dynamic schema에 예약되어 있습니다.
|
|
57
|
+
graph는 callback 또는 resolver-backed semantics가 필요하다는 사실을 기록하며, 이것을 static predicate인 척하지 않습니다.
|
|
58
|
+
|
|
59
|
+
## Runtime Compiler
|
|
60
|
+
|
|
61
|
+
compiled guard는 optimized Sea-of-Nodes graph에서 boolean predicate를 방출하고, failed value용 schema-aware diagnostic collector를 함께 생성합니다.
|
|
62
|
+
runtime `is()`는 plan-owned schema-specialized kernel을 사용해 recursive node dispatch와 scratch buffer churn을 피합니다.
|
|
63
|
+
`check()`는 먼저 plan predicate로 pass/fail verdict를 얻습니다.
|
|
64
|
+
successful value는 diagnostic collection을 건너뛰고, failed value는 diagnostic collector를 replay해서 path와 issue code를 만듭니다.
|
|
65
|
+
|
|
66
|
+
user-controlled literal, regexp, object key, keyset, dynamic schema, diagnostic name은 generated factory가 capture한 side table에 둡니다.
|
|
67
|
+
generated source에는 numeric side-table index, fixed helper string, sanitized function name만 들어갑니다.
|
|
68
|
+
|
|
69
|
+
semantic이 local한 scalar node는 direct JavaScript test로 emit됩니다.
|
|
70
|
+
finite-number check, integer check, string length bound, literal equality, regexp test는 generated hot path에서 helper call 없이 낮아집니다.
|
|
71
|
+
|
|
72
|
+
array와 record IR node는 indexed loop를 emit합니다.
|
|
73
|
+
static child schema는 optimized graph에서 해당 loop 안으로 inline됩니다.
|
|
74
|
+
작은 scalar 또는 union element contract에서 function-call boundary를 피하기 위해서입니다.
|
|
75
|
+
tuple node는 descriptor-based element access를 보존하고, dynamic edge는 ordinary guard execution과 같은 IR-backed runtime fallback을 사용합니다.
|
|
76
|
+
따라서 `lazy`와 `refine`의 동작이 유지됩니다.
|
|
77
|
+
|
|
78
|
+
strict object IR은 두 가지 shape으로 emit됩니다.
|
|
79
|
+
모든 declared key가 required이면 generated validator는 field descriptor read보다 먼저 strict-key count를 실행합니다.
|
|
80
|
+
`Object.getOwnPropertyNames(value).length`를 declared key count와 비교하고, `Object.getOwnPropertySymbols(value).length === 0`을 요구합니다.
|
|
81
|
+
V8은 generic `Reflect.ownKeys` count보다 이 count-only path를 더 잘 최적화하고, obvious extra-key object를 field descriptor를 만지기 전에 거부할 수 있습니다.
|
|
82
|
+
optional strict object는 missing optional key를 final key count만으로 구분할 수 없으므로 full own-key membership scan을 emit합니다.
|
|
83
|
+
|
|
84
|
+
`compile(..., { mode: "unsafe" })`와 `emitAotModule(..., { mode: "unsafe" })`는 generated predicate를 trusted-data code shape으로 전환합니다.
|
|
85
|
+
schema가 `undefined`를 거부하는 required object field는 descriptor 또는 own-key check 없이 direct `value[key]` load를 사용합니다.
|
|
86
|
+
`undefined`를 허용할 수 있는 required field는 missing required key가 valid `undefined` value로 collapse되지 않도록 own-key presence guard를 유지합니다.
|
|
87
|
+
optional field는 present non-`undefined` value에 direct-load fast path를 쓰고, ambiguous `undefined` case에서만 own-key check로 fallback합니다.
|
|
88
|
+
|
|
89
|
+
unsafe array, tuple, record, discriminant path도 direct load를 선호합니다.
|
|
90
|
+
strict object는 own-key array를 할당하는 대신 `for...in` own-enumerable key loop를 사용합니다.
|
|
91
|
+
ASCII identifier object key는 `value.id` 같은 dot-property load로 emit되고, 나머지는 escaped string-literal bracket load로 emit됩니다.
|
|
92
|
+
이는 의도적으로 safe mode와 같은 적대적 입력 방어가 아닙니다.
|
|
93
|
+
getter가 실행될 수 있고, prototype-backed value가 accepted될 수 있으며, symbol 또는 non-enumerable strict extra가 거부되지 않고, static property name이 unsafe generated predicate source에 나타날 수 있습니다.
|
|
94
|
+
|
|
95
|
+
`mode: "unchecked"`는 unsafe direct-read shape을 유지하면서 strict extra-key loop를 제거합니다.
|
|
96
|
+
caller가 이미 정규화한 object를 위한 trusted-shape path입니다.
|
|
97
|
+
이 모드에서 strict object는 더 이상 extra key를 거부하지 않습니다.
|
|
98
|
+
|
|
99
|
+
fast mode는 successful compiled `check()` result에서 `Object.freeze()`도 제거합니다.
|
|
100
|
+
반환 object는 동일한 `{ ok: true, value }` shape을 유지하지만 의도적으로 freeze되지 않습니다.
|
|
101
|
+
failed diagnostic은 success hot path 밖에 있고 reporting을 위해 보존되는 일이 많으므로 계속 freeze됩니다.
|
|
102
|
+
fast mode의 object diagnostic은 predicate와 같은 direct-read contract에서 생성됩니다.
|
|
103
|
+
required field는 `value.key`로 읽고, optional field는 direct load 뒤 `undefined`일 때 own-key fallback을 사용하며, unsafe strict object는 own enumerable string key를 scan하고, unchecked strict object는 strict-key diagnostic scan을 건너뜁니다.
|
|
104
|
+
fast mode의 array와 tuple diagnostic은 descriptor probe 대신 direct index로 item을 읽습니다.
|
|
105
|
+
record diagnostic은 `record[key]`로 읽습니다.
|
|
106
|
+
unchecked mode는 inherited enumerable key를 의도적으로 보이게 둡니다.
|
|
107
|
+
discriminant diagnostic은 tag를 직접 읽고 literal string case를 strict equality로 비교합니다.
|
|
108
|
+
|
|
109
|
+
## 재귀
|
|
110
|
+
|
|
111
|
+
lazy schema는 guard instance마다 getter를 한 번 resolve합니다.
|
|
112
|
+
따라서 recursive validation은 stable schema identity를 보고, 반복 validation이 recursive schema graph를 다시 만들지 않습니다.
|
|
113
|
+
|
|
114
|
+
recursive validation은 root-local active pair table을 사용합니다.
|
|
115
|
+
key는 runtime object identity와 schema identity의 pair입니다.
|
|
116
|
+
같은 schema/value pair에 다시 들어오면 그 edge를 short-circuit합니다.
|
|
117
|
+
이 방식으로 cyclic object graph도 유한하게 검증하면서, outer frame에서는 원래 object field를 계속 검사합니다.
|
|
118
|
+
|
|
119
|
+
compiled `lazy`와 `refine` fallback은 같은 IR-backed runtime path를 사용하므로 recursive behavior가 execution engine 사이에서 일관됩니다.
|
|
120
|
+
|
|
121
|
+
`checkFirst()`는 별도의 generated collector를 사용합니다.
|
|
122
|
+
첫 diagnostic이 확정되는 즉시 frozen issue 하나를 반환하며, full `check()` collector를 끝까지 실행한 뒤 issue array를 자르지 않습니다.
|
|
123
|
+
|
|
124
|
+
## JSON Schema Export
|
|
125
|
+
|
|
126
|
+
JSON Schema export는 TypeSea schema를 JSON-compatible input value 위에서 의미 손실 없이 표현할 수 있을 때만 성공합니다.
|
|
127
|
+
runtime-only concept는 typed `Result` error를 반환합니다.
|
|
128
|
+
|
|
129
|
+
export diagnostic은 모든 것을 parent container로 collapse하지 않고 failed child slot에 path를 유지합니다.
|
|
130
|
+
따라서 nested unsupported schema도 schema tree를 수동으로 재구성하지 않고 바로 조치할 수 있습니다.
|
|
131
|
+
|
|
132
|
+
literal check는 runtime-plan과 compiled path 모두에서 `Object.is`를 사용합니다.
|
|
133
|
+
diagnostic도 `-0`을 포함해 같은 literal formatting을 사용하므로 compiled와 runtime-plan `check()` result가 test에서 byte-for-byte로 비교됩니다.
|
|
134
|
+
|
|
135
|
+
## Benchmark 범위
|
|
136
|
+
|
|
137
|
+
benchmark suite는 두 질문을 분리합니다.
|
|
138
|
+
|
|
139
|
+
- `compile.bench.ts`는 같은 TypeSea schema를 대상으로 TypeSea runtime-plan validator와 compiled validator를 비교합니다.
|
|
140
|
+
- `ecosystem.bench.ts`는 하나의 JSON-compatible strict-object contract를 대상으로 TypeSea runtime-plan, TypeSea compiled, Zod, Valibot, Ajv를 비교합니다.
|
|
141
|
+
|
|
142
|
+
Zod, Valibot, Ajv는 측정용 dev dependency입니다.
|
|
143
|
+
`src`에서 import하지 않으며, package policy는 release 전에 runtime, peer, optional, bundled dependency field를 거부합니다.
|
|
144
|
+
|
|
145
|
+
2026-07-05 KST의 마지막 로컬 벤치마크는 JSON-compatible strict-object benchmark에서 아래 ecosystem path를 보고했습니다.
|
|
146
|
+
|
|
147
|
+
| Case | TypeSea runtime plan | TypeSea compiled safe | TypeSea compiled unsafe | TypeSea compiled unchecked | Ajv compiled |
|
|
148
|
+
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
149
|
+
| Valid `is()` | 478,576 hz | 5,109,602 hz | 36,777,097 hz | 42,620,570 hz | 4,238,036 hz |
|
|
150
|
+
| Valid `check()` | 424,989 hz | 4,642,948 hz | 37,184,199 hz | 42,487,325 hz | 4,338,063 hz |
|
|
151
|
+
| Invalid `is()` | 3,325,603 hz | 43,094,061 hz | 50,738,235 hz | 50,898,012 hz | 30,535,761 hz |
|
|
152
|
+
| Invalid `check()` | 405,590 hz | 2,107,460 hz | 3,186,702 hz | 3,509,673 hz | 29,951,403 hz |
|
|
153
|
+
|
|
154
|
+
benchmark number는 machine-local telemetry입니다.
|
|
155
|
+
regression을 잡는 데 유용하지만 고정된 throughput floor를 약속하지 않습니다.
|
|
156
|
+
unsafe와 unchecked number는 safe mode와 hostile-input equivalent가 아닙니다.
|