@mikrojs/native 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CMakeLists.txt +62 -1
  2. package/dist/index.d.ts +17 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/result/native-result.node-shim.d.ts +3 -0
  7. package/dist/runtime/result/native-result.node-shim.d.ts.map +1 -0
  8. package/dist/runtime/result/native-result.node-shim.js +41 -0
  9. package/dist/runtime/result/native-result.node-shim.js.map +1 -0
  10. package/dist/runtime/result/types.d.ts +55 -0
  11. package/dist/runtime/result/types.d.ts.map +1 -0
  12. package/dist/runtime/result/types.js +2 -0
  13. package/dist/runtime/result/types.js.map +1 -0
  14. package/dist/runtime/schema/core.d.ts +115 -0
  15. package/dist/runtime/schema/core.d.ts.map +1 -0
  16. package/dist/runtime/schema/core.js +259 -0
  17. package/dist/runtime/schema/core.js.map +1 -0
  18. package/dist/runtime/schema/shared.d.ts +54 -0
  19. package/dist/runtime/schema/shared.d.ts.map +1 -0
  20. package/dist/runtime/schema/shared.js +489 -0
  21. package/dist/runtime/schema/shared.js.map +1 -0
  22. package/dist/types.d.ts +7 -0
  23. package/dist/types.d.ts.map +1 -1
  24. package/include/mikrojs/cbor_helpers.h +20 -0
  25. package/include/mikrojs/mem.h +11 -0
  26. package/include/mikrojs/mikrojs.h +2 -1
  27. package/include/mikrojs/ota_client.h +342 -0
  28. package/include/mikrojs/ota_config.h +100 -0
  29. package/include/mikrojs/ota_env.h +192 -0
  30. package/include/mikrojs/ota_js_hooks.h +71 -0
  31. package/include/mikrojs/ota_policy.h +131 -0
  32. package/include/mikrojs/ota_slots.h +47 -0
  33. package/include/mikrojs/sys_codec.h +61 -0
  34. package/package.json +7 -5
  35. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  36. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  37. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  38. package/runtime/internal.d.ts +22 -16
  39. package/runtime/kv/shared.ts +11 -5
  40. package/runtime/kv/types.ts +4 -4
  41. package/runtime/ota/client.ts +12 -51
  42. package/runtime/ota/config.ts +18 -0
  43. package/runtime/ota/ota.ts +28 -70
  44. package/runtime/ota/types.ts +220 -2
  45. package/runtime/schema/core.ts +539 -0
  46. package/runtime/schema/schema.ts +36 -314
  47. package/runtime/schema/shared.ts +494 -0
  48. package/runtime/schema/types.ts +84 -12
  49. package/scripts/bundle-runtime.js +33 -0
  50. package/scripts/gen-checkin-fixtures.js +323 -0
  51. package/src/builtins.cpp +7 -8
  52. package/src/fs.cpp +3 -0
  53. package/src/mem.cpp +38 -0
  54. package/src/mik_abort.cpp +8 -1
  55. package/src/mik_cbor.cpp +43 -5
  56. package/src/mik_inspect.cpp +128 -22
  57. package/src/mik_ota_client.cpp +1230 -0
  58. package/src/mik_ota_config.cpp +296 -0
  59. package/src/mik_ota_js_hooks.cpp +190 -0
  60. package/src/mik_ota_policy.cpp +419 -0
  61. package/src/mik_ota_slots.cpp +249 -0
  62. package/src/mik_repl.cpp +9 -3
  63. package/src/mik_result.cpp +3 -1
  64. package/src/mik_sys_codec.cpp +167 -0
  65. package/src/mikrojs.cpp +15 -0
  66. package/src/modules.cpp +32 -13
  67. package/runtime/ota/client-impl.ts +0 -590
  68. package/runtime/ota/policy.ts +0 -299
@@ -0,0 +1,539 @@
1
+ /* Core schema machinery: types, constructors, the validator, and
2
+ * applyDefaults. Dependency-free so hosts (CLI, registries) can import it via
3
+ * shared.ts without resolving mikro/* builtins; mikro/schema re-exports it and
4
+ * adds the Result-returning parse(). */
5
+
6
+ function err<E>(error: E) {
7
+ return {ok: false as const, error}
8
+ }
9
+
10
+ export const SchemaError = {
11
+ ValidationFailed: (message: string, path: string) =>
12
+ ({name: 'ValidationFailed', message, path}) as const,
13
+ }
14
+ export type SchemaError = ReturnType<typeof SchemaError.ValidationFailed>
15
+
16
+ // ── Schema types ────────────────────────────────────────────────────
17
+
18
+ type Primitive = string | number | boolean
19
+
20
+ /* The `default` annotation is stored as an extra node property so a schema
21
+ * serializes to JSON as-is; it is typed precisely on the constructor options
22
+ * and loosely on the node, which keeps Infer free of recursive
23
+ * instantiations. */
24
+
25
+ export interface StringSchema {
26
+ readonly kind: 'string'
27
+ readonly default?: string
28
+ }
29
+
30
+ export interface NumberSchema {
31
+ readonly kind: 'number'
32
+ readonly default?: number
33
+ }
34
+
35
+ export interface BooleanSchema {
36
+ readonly kind: 'boolean'
37
+ readonly default?: boolean
38
+ }
39
+
40
+ export interface UnknownSchema {
41
+ readonly kind: 'unknown'
42
+ }
43
+
44
+ export interface LiteralSchema<T extends Primitive = Primitive> {
45
+ readonly kind: 'literal'
46
+ readonly value: T
47
+ readonly default?: T
48
+ }
49
+
50
+ export interface ArraySchema<S extends Schema = Schema> {
51
+ readonly kind: 'array'
52
+ readonly element: S
53
+ readonly default?: unknown
54
+ }
55
+
56
+ export interface ObjectSchema<Shape extends Record<string, Schema> = Record<string, Schema>> {
57
+ readonly kind: 'object'
58
+ readonly shape: Shape
59
+ }
60
+
61
+ export interface OptionalSchema<S extends Schema = Schema> {
62
+ readonly kind: 'optional'
63
+ readonly inner: S
64
+ }
65
+
66
+ export interface TupleSchema<Elements extends readonly Schema[] = readonly Schema[]> {
67
+ readonly kind: 'tuple'
68
+ readonly elements: Elements
69
+ readonly default?: unknown
70
+ }
71
+
72
+ export interface UnionSchema<Members extends readonly Schema[] = readonly Schema[]> {
73
+ readonly kind: 'union'
74
+ readonly members: Members
75
+ readonly default?: unknown
76
+ }
77
+
78
+ export interface TaggedUnionSchema<
79
+ Key extends string = string,
80
+ Branches extends Record<string, ObjectSchema> = Record<string, ObjectSchema>,
81
+ > {
82
+ readonly kind: 'taggedUnion'
83
+ readonly key: Key
84
+ readonly branches: Branches
85
+ readonly default?: unknown
86
+ }
87
+
88
+ export type Schema =
89
+ | StringSchema
90
+ | NumberSchema
91
+ | BooleanSchema
92
+ | UnknownSchema
93
+ | LiteralSchema
94
+ | ArraySchema
95
+ | ObjectSchema
96
+ | OptionalSchema
97
+ | TupleSchema
98
+ | UnionSchema
99
+ | TaggedUnionSchema
100
+
101
+ // ── Type inference ──────────────────────────────────────────────────
102
+
103
+ type Simplify<T> = {[K in keyof T]: T[K]} & {}
104
+
105
+ export type Infer<S> = S extends StringSchema
106
+ ? string
107
+ : S extends NumberSchema
108
+ ? number
109
+ : S extends BooleanSchema
110
+ ? boolean
111
+ : S extends UnknownSchema
112
+ ? unknown
113
+ : S extends LiteralSchema<infer T>
114
+ ? T
115
+ : S extends ArraySchema<infer E>
116
+ ? ArraySchema extends S
117
+ ? []
118
+ : Infer<E>[]
119
+ : S extends ObjectSchema<infer Shape>
120
+ ? ObjectSchema extends S
121
+ ? object
122
+ : Simplify<InferObject<Shape>>
123
+ : S extends TupleSchema<infer Elements>
124
+ ? InferTuple<Elements>
125
+ : S extends OptionalSchema<infer Inner>
126
+ ? OptionalSchema extends S
127
+ ? OptionalSchema
128
+ : Infer<Inner> | undefined
129
+ : S extends UnionSchema<infer Members>
130
+ ? InferUnion<Members>
131
+ : S extends TaggedUnionSchema<infer Key, infer Branches>
132
+ ? InferTaggedUnion<Key, Branches>
133
+ : never
134
+
135
+ type InferObject<Shape> = {
136
+ [K in keyof Shape as Shape[K] extends OptionalSchema ? never : K]: Infer<Shape[K]>
137
+ } & {
138
+ [K in keyof Shape as Shape[K] extends OptionalSchema ? K : never]?: Infer<Shape[K]>
139
+ }
140
+
141
+ type InferTuple<Elements> = Elements extends readonly [infer Head, ...infer Tail]
142
+ ? [Infer<Head>, ...InferTuple<Tail>]
143
+ : []
144
+
145
+ type InferUnion<Members> = Members extends readonly [infer Head, ...infer Tail]
146
+ ? Infer<Head> | InferUnion<Tail>
147
+ : never
148
+
149
+ type InferTaggedUnion<Key extends string, Branches> = {
150
+ [Tag in keyof Branches & string]: {[K in Key]: Tag} & Infer<Branches[Tag]>
151
+ }[keyof Branches & string]
152
+
153
+ /* The read type: what applyDefaults alone can hand back. A field defaults
154
+ * cannot fill is optional here, while Infer keeps it required: Infer is the
155
+ * write type, where an operator must supply it. */
156
+ export type InferRead<S> =
157
+ S extends ObjectSchema<infer Shape>
158
+ ? ObjectSchema extends S
159
+ ? object
160
+ : Simplify<InferReadObject<Shape>>
161
+ : Infer<S>
162
+
163
+ /* What the materialized defaults always contain: a node carrying its own
164
+ * default, or a plain object whose fields ALL fill (or are optional). A
165
+ * defaultless array and a partially fillable object are omitted whole, so
166
+ * their fields read as absent until a document supplies them. Must stay in
167
+ * lockstep with materializeDefaults in shared.ts. */
168
+ type Filled<S> = S extends {default: unknown}
169
+ ? true
170
+ : S extends OptionalSchema
171
+ ? false
172
+ : S extends ObjectSchema<infer Shape>
173
+ ? ObjectSchema extends S
174
+ ? false
175
+ : AllFilled<Shape>
176
+ : false
177
+
178
+ /* Every field fills or is optional; an empty shape fills as {}. */
179
+ type AllFilled<Shape> = false extends {
180
+ [K in keyof Shape]: Shape[K] extends OptionalSchema ? true : Filled<Shape[K]>
181
+ }[keyof Shape]
182
+ ? false
183
+ : true
184
+
185
+ type InferReadObject<Shape> = {
186
+ [K in keyof Shape as Filled<Shape[K]> extends true ? K : never]: InferRead<Shape[K]>
187
+ } & {
188
+ [K in keyof Shape as Filled<Shape[K]> extends true ? never : K]?: InferRead<Shape[K]>
189
+ }
190
+
191
+ // ── Schema constructors ─────────────────────────────────────────────
192
+
193
+ export interface ScalarOptions<T> {
194
+ readonly default?: T
195
+ }
196
+
197
+ export interface DefaultOption<T> {
198
+ readonly default?: T
199
+ }
200
+
201
+ /* A node interface types `default` as optional, so a defaulted node and a bare
202
+ * one are the same type; the constructors record the annotation in their
203
+ * return type instead, which is what lets InferRead see it. D is the inferred
204
+ * type of the `default` option, undefined when none was written. */
205
+ type Defaulted<S, D> = [D] extends [undefined] ? S : S & {readonly default: unknown}
206
+
207
+ /* Defaults below a wholesale unit never fill: applyDefaults replaces the unit
208
+ * whole, so only a unit-level default applies. Rejected where they are written
209
+ * rather than at the validation that later misses the field. The walk stops at
210
+ * a nested unit's own default, since that unit's constructor already cleared
211
+ * everything under it. */
212
+ function rejectInnerDefaults(node: Schema, path: string, unit: string, self: string): void {
213
+ if ((node as {default?: unknown}).default !== undefined) {
214
+ throw new TypeError(
215
+ `a default under ${unit} never applies; give ${self} itself a whole-value ` +
216
+ `default instead (found at ${path})`,
217
+ )
218
+ }
219
+ if (node.kind === 'object') {
220
+ const keys = Object.keys(node.shape)
221
+ for (let i = 0; i < keys.length; i++) {
222
+ rejectInnerDefaults(node.shape[keys[i]!]!, `${path}.${keys[i]!}`, unit, self)
223
+ }
224
+ } else if (node.kind === 'optional') {
225
+ // optional() rejects an inner default itself, so this only reaches what it
226
+ // wraps without reporting the same node twice.
227
+ rejectInnerDefaults(node.inner, path, unit, self)
228
+ }
229
+ }
230
+
231
+ /* Copies the annotation onto the node and rejects a `default` the node itself
232
+ * would not accept, so a bad default fails where it is written. */
233
+ function annotate<S extends Schema>(node: S, options?: {default?: unknown}): S {
234
+ if (options === undefined) return node
235
+ const out = node as {default?: unknown}
236
+ if (options.default !== undefined) {
237
+ out.default = options.default
238
+ const result = validate(node, options.default, '')
239
+ if (result !== null) {
240
+ throw new TypeError(`schema default does not match the schema: ${result.error.message}`)
241
+ }
242
+ }
243
+ return node
244
+ }
245
+
246
+ export function string<D extends string | undefined = undefined>(
247
+ options?: ScalarOptions<D>,
248
+ ): Defaulted<StringSchema, D> {
249
+ return annotate<StringSchema>({kind: 'string'}, options) as Defaulted<StringSchema, D>
250
+ }
251
+
252
+ export function number<D extends number | undefined = undefined>(
253
+ options?: ScalarOptions<D>,
254
+ ): Defaulted<NumberSchema, D> {
255
+ return annotate<NumberSchema>({kind: 'number'}, options) as Defaulted<NumberSchema, D>
256
+ }
257
+
258
+ export function boolean<D extends boolean | undefined = undefined>(
259
+ options?: ScalarOptions<D>,
260
+ ): Defaulted<BooleanSchema, D> {
261
+ return annotate<BooleanSchema>({kind: 'boolean'}, options) as Defaulted<BooleanSchema, D>
262
+ }
263
+
264
+ export function unknown(): UnknownSchema {
265
+ return {kind: 'unknown'}
266
+ }
267
+
268
+ export function literal<T extends Primitive, D extends T | undefined = undefined>(
269
+ value: T,
270
+ options?: ScalarOptions<D>,
271
+ ): Defaulted<LiteralSchema<T>, D> {
272
+ return annotate<LiteralSchema<T>>({kind: 'literal', value}, options) as Defaulted<
273
+ LiteralSchema<T>,
274
+ D
275
+ >
276
+ }
277
+
278
+ export function array<S extends Schema, D extends NoInfer<Infer<S>>[] | undefined = undefined>(
279
+ element: S,
280
+ options?: DefaultOption<D>,
281
+ ): Defaulted<ArraySchema<S>, D> {
282
+ rejectInnerDefaults(element, '[]', 'an array', 'the array')
283
+ return annotate<ArraySchema<S>>({kind: 'array', element}, options) as Defaulted<ArraySchema<S>, D>
284
+ }
285
+
286
+ export function object<Shape extends Record<string, Schema>>(
287
+ shape: Shape,
288
+ options?: DefaultOption<never>,
289
+ ): ObjectSchema<Shape> {
290
+ if (options?.default !== undefined) {
291
+ throw new TypeError(
292
+ "an object's defaults compose from its fields; declare defaults on the fields",
293
+ )
294
+ }
295
+ return {kind: 'object', shape}
296
+ }
297
+
298
+ export function tuple<
299
+ Elements extends readonly Schema[],
300
+ D extends NoInfer<Infer<TupleSchema<Elements>>> | undefined = undefined,
301
+ >(elements: [...Elements], options?: DefaultOption<D>): Defaulted<TupleSchema<Elements>, D> {
302
+ for (let i = 0; i < elements.length; i++) {
303
+ rejectInnerDefaults(elements[i]!, `[${i}]`, 'a tuple', 'the tuple')
304
+ }
305
+ return annotate<TupleSchema<Elements>>({kind: 'tuple', elements}, options) as Defaulted<
306
+ TupleSchema<Elements>,
307
+ D
308
+ >
309
+ }
310
+
311
+ export function optional<S extends Schema>(inner: S): OptionalSchema<S> {
312
+ if ((inner as {default?: unknown}).default !== undefined) {
313
+ throw new TypeError('optional() cannot wrap a schema with a default')
314
+ }
315
+ return {kind: 'optional', inner}
316
+ }
317
+
318
+ export function union<
319
+ Members extends readonly Schema[],
320
+ D extends NoInfer<Infer<UnionSchema<Members>>> | undefined = undefined,
321
+ >(members: [...Members], options?: DefaultOption<D>): Defaulted<UnionSchema<Members>, D> {
322
+ for (let i = 0; i < members.length; i++) {
323
+ rejectInnerDefaults(members[i]!, `[${i}]`, 'a union', 'the union')
324
+ }
325
+ return annotate<UnionSchema<Members>>({kind: 'union', members}, options) as Defaulted<
326
+ UnionSchema<Members>,
327
+ D
328
+ >
329
+ }
330
+
331
+ export function taggedUnion<
332
+ Key extends string,
333
+ Branches extends Record<string, ObjectSchema>,
334
+ D extends NoInfer<Infer<TaggedUnionSchema<Key, Branches>>> | undefined = undefined,
335
+ >(
336
+ key: Key,
337
+ branches: Branches,
338
+ options?: DefaultOption<D>,
339
+ ): Defaulted<TaggedUnionSchema<Key, Branches>, D> {
340
+ const tags = Object.keys(branches)
341
+ for (let i = 0; i < tags.length; i++) {
342
+ rejectInnerDefaults(branches[tags[i]!]!, `.${tags[i]!}`, 'a taggedUnion', 'the union')
343
+ }
344
+ return annotate<TaggedUnionSchema<Key, Branches>>(
345
+ {kind: 'taggedUnion', key, branches},
346
+ options,
347
+ ) as Defaulted<TaggedUnionSchema<Key, Branches>, D>
348
+ }
349
+
350
+ // ── Defaults ────────────────────────────────────────────────────────
351
+
352
+ /* Builds the effective value: schema defaults with `value` layered over them.
353
+ * Objects are structure and always materialize (recursing per field, unknown
354
+ * keys dropped); every other node is replaced wholesale by a present value, so
355
+ * defaults inside array elements or union branches are form hints, not fills.
356
+ * The result is unvalidated — a missing required field stays missing for
357
+ * parse() to report. */
358
+ export function applyDefaults(schema: Schema, value: unknown): unknown {
359
+ switch (schema.kind) {
360
+ case 'object': {
361
+ // A present non-object stays as-is so validation rejects it; replacing
362
+ // it with {} here would make `{mqtt: 42}` validate clean and never
363
+ // record a configError.
364
+ if (
365
+ value !== undefined &&
366
+ (typeof value !== 'object' || value === null || Array.isArray(value))
367
+ ) {
368
+ return value
369
+ }
370
+ const src = value === undefined ? {} : (value as Record<string, unknown>)
371
+ const out: Record<string, unknown> = {}
372
+ const keys = Object.keys(schema.shape)
373
+ for (let i = 0; i < keys.length; i++) {
374
+ const key = keys[i]!
375
+ const field = schema.shape[key]!
376
+ // hasOwn, not indexing: a shape key like "constructor" must read as
377
+ // absent, not as the inherited prototype member.
378
+ const raw = Object.hasOwn(src, key) ? src[key] : undefined
379
+ if (field.kind === 'optional') {
380
+ if (raw !== undefined) out[key] = raw
381
+ } else {
382
+ const child = applyDefaults(field, raw)
383
+ if (child !== undefined) out[key] = child
384
+ }
385
+ }
386
+ return out
387
+ }
388
+ case 'array':
389
+ if (value !== undefined) return value
390
+ return schema.default !== undefined ? schema.default : []
391
+ case 'unknown':
392
+ case 'optional':
393
+ return value
394
+ default:
395
+ return value !== undefined ? value : (schema as {default?: unknown}).default
396
+ }
397
+ }
398
+
399
+ // ── Parse ───────────────────────────────────────────────────────────
400
+
401
+ function typeOf(value: unknown): string {
402
+ if (value === null) return 'null'
403
+ if (Array.isArray(value)) return 'array'
404
+ return typeof value
405
+ }
406
+
407
+ export function validate(
408
+ schema: Schema,
409
+ value: unknown,
410
+ path: string,
411
+ ): ReturnType<typeof err<SchemaError>> | null {
412
+ switch (schema.kind) {
413
+ case 'string':
414
+ if (typeof value !== 'string')
415
+ return err(SchemaError.ValidationFailed(`expected string, got ${typeOf(value)}`, path))
416
+ return null
417
+
418
+ case 'number':
419
+ if (typeof value !== 'number' || Number.isNaN(value))
420
+ return err(SchemaError.ValidationFailed(`expected number, got ${typeOf(value)}`, path))
421
+ return null
422
+
423
+ case 'boolean':
424
+ if (typeof value !== 'boolean')
425
+ return err(SchemaError.ValidationFailed(`expected boolean, got ${typeOf(value)}`, path))
426
+ return null
427
+
428
+ case 'unknown':
429
+ return null
430
+
431
+ case 'literal':
432
+ if (value !== schema.value)
433
+ return err(
434
+ SchemaError.ValidationFailed(
435
+ `expected ${JSON.stringify(schema.value)}, got ${JSON.stringify(value)}`,
436
+ path,
437
+ ),
438
+ )
439
+ return null
440
+
441
+ case 'array': {
442
+ if (!Array.isArray(value))
443
+ return err(SchemaError.ValidationFailed(`expected array, got ${typeOf(value)}`, path))
444
+ for (let i = 0; i < value.length; i++) {
445
+ const result = validate(schema.element, value[i], `${path}[${i}]`)
446
+ if (result !== null) return result
447
+ }
448
+ return null
449
+ }
450
+
451
+ case 'object': {
452
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
453
+ return err(SchemaError.ValidationFailed(`expected object, got ${typeOf(value)}`, path))
454
+ const obj = value as Record<string, unknown>
455
+ const keys = Object.keys(schema.shape)
456
+ for (let i = 0; i < keys.length; i++) {
457
+ const key = keys[i]!
458
+ const fieldSchema = schema.shape[key]!
459
+ const fieldPath = `${path}.${key}`
460
+ // hasOwn, like the taggedUnion dispatch: an inherited "constructor"
461
+ // must not stand in for a field.
462
+ if (fieldSchema.kind === 'optional') {
463
+ if (Object.hasOwn(obj, key)) {
464
+ const result = validate(fieldSchema, obj[key], fieldPath)
465
+ if (result !== null) return result
466
+ }
467
+ } else {
468
+ if (!Object.hasOwn(obj, key))
469
+ return err(SchemaError.ValidationFailed(`missing required field`, fieldPath))
470
+ const result = validate(fieldSchema, obj[key], fieldPath)
471
+ if (result !== null) return result
472
+ }
473
+ }
474
+ return null
475
+ }
476
+
477
+ case 'tuple': {
478
+ if (!Array.isArray(value))
479
+ return err(SchemaError.ValidationFailed(`expected array, got ${typeOf(value)}`, path))
480
+ if (value.length !== schema.elements.length)
481
+ return err(
482
+ SchemaError.ValidationFailed(
483
+ `expected ${schema.elements.length} elements, got ${value.length}`,
484
+ path,
485
+ ),
486
+ )
487
+ for (let i = 0; i < schema.elements.length; i++) {
488
+ const result = validate(schema.elements[i]!, value[i], `${path}[${i}]`)
489
+ if (result !== null) return result
490
+ }
491
+ return null
492
+ }
493
+
494
+ case 'optional': {
495
+ if (value === undefined) return null
496
+ return validate(schema.inner, value, path)
497
+ }
498
+
499
+ case 'union': {
500
+ for (let i = 0; i < schema.members.length; i++) {
501
+ const result = validate(schema.members[i]!, value, path)
502
+ if (result === null) return null
503
+ }
504
+ return err(SchemaError.ValidationFailed(`value did not match any union member`, path))
505
+ }
506
+
507
+ case 'taggedUnion': {
508
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
509
+ return err(SchemaError.ValidationFailed(`expected object, got ${typeOf(value)}`, path))
510
+ const obj = value as Record<string, unknown>
511
+ const tag = obj[schema.key]
512
+ if (tag === undefined)
513
+ return err(
514
+ SchemaError.ValidationFailed(`missing discriminator field`, `${path}.${schema.key}`),
515
+ )
516
+ if (typeof tag !== 'string' && typeof tag !== 'number' && typeof tag !== 'boolean')
517
+ return err(
518
+ SchemaError.ValidationFailed(
519
+ `expected primitive discriminator, got ${typeOf(tag)}`,
520
+ `${path}.${schema.key}`,
521
+ ),
522
+ )
523
+ // hasOwn, not indexing: a tag like "constructor" must not resolve to
524
+ // an inherited property and validate against garbage.
525
+ const branch = Object.hasOwn(schema.branches, tag as string)
526
+ ? schema.branches[tag as string]
527
+ : undefined
528
+ if (branch === undefined)
529
+ return err(
530
+ SchemaError.ValidationFailed(
531
+ `unknown tag ${JSON.stringify(tag)}`,
532
+ `${path}.${schema.key}`,
533
+ ),
534
+ )
535
+ return validate(branch, value, path)
536
+ }
537
+ }
538
+ return err(SchemaError.ValidationFailed(`unknown schema kind: ${(schema as any).kind}`, path))
539
+ }