ReviewOS

also looking at this

craft-native/craft

fix(types): declare canonical native bridge error codes

#304
Merged glennmichael123 wants to merge codex/typed-native-error-codes into main
3 files +86 -1
packages/typescript/README.mdmodified+17-0
Changes to packages/typescript/README.md
@@ -453,6 +453,23 @@ See [benchmarks](../../benchmarks) for detailed performance comparisons.
453453- **Zero dependencies** - Pure Node.js APIs only
454454- **Better DX** - Hot reload and dev tools built-in
455455
456## Native bridge error codes
457
458The mobile bridge declarations in `types/craft.d.ts` distinguish canonical
459`NativeBridgeErrorCode` values from the broader compatible `CraftErrorCode` union.
460The native wire vocabulary comes from `packages/zig/src/bridge_error.zig`; SDK
461parity tests fail if the declaration drifts. It includes `INVALID_PARAMETER`,
462`PLATFORM_NOT_SUPPORTED`, `NATIVE_CALL_FAILED`, `WEBVIEW_HANDLE_NOT_SET`, `BUSY`,
463and `CAPABILITY_DISABLED`.
464
465Existing mobile/shim names such as `INVALID_PARAMS` and `NOT_AVAILABLE` remain
466accepted by `CraftErrorCode` for source compatibility. They are not aliases that
467the SDK rewrites on the wire. Applications supporting multiple host versions
468should handle the codes those hosts actually return; new native error handling
469should use the canonical spellings. A capability disabled by app configuration
470is distinct from a user denying permission, and `BUSY` is distinct from a failed
471native call.
472
456473## Requirements
457474
458475- Bun >= 1.0.0 (for development)
packages/typescript/src/__tests__/native-error-codes.test.tsadded+44-0
Changes to packages/typescript/src/__tests__/native-error-codes.test.ts
@@ -0,0 +1,44 @@
1import { expect, test } from 'bun:test'
2import { copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3import { tmpdir } from 'node:os'
4import { join, resolve } from 'node:path'
5
6const sdk = resolve(import.meta.dir, '../..')
7const declarations = join(sdk, 'types/craft.d.ts')
8
9test('mobile native error types exactly match the Zig wire vocabulary', () => {
10 const zig = readFileSync(join(sdk, '../zig/src/bridge_error.zig'), 'utf8')
11 const mapping = zig.match(/pub fn errorCodeString[\s\S]*?\n}/)?.[0]
12 expect(mapping).toBeDefined()
13 const nativeCodes = [...mapping!.matchAll(/BridgeError\.\w+\s*=>\s*"([A-Z_]+)"/g)].map(match => match[1]).sort()
14 const types = readFileSync(declarations, 'utf8')
15 const declaration = types.match(/export type NativeBridgeErrorCode\s*=([\s\S]*?);/)?.[1]
16 expect(declaration).toBeDefined()
17 const typedCodes = [...declaration!.matchAll(/'([A-Z_]+)'/g)].map(match => match[1]).sort()
18 expect(nativeCodes.length).toBeGreaterThan(0)
19 expect(typedCodes).toEqual(nativeCodes)
20 expect(new Set(typedCodes).size).toBe(typedCodes.length)
21})
22
23test('isolated mobile consumers accept native and legacy codes but reject misspellings', () => {
24 const root = mkdtempSync(join(tmpdir(), 'craft-error-types-'))
25 try {
26 copyFileSync(declarations, join(root, 'craft.d.ts'))
27 writeFileSync(join(root, 'consumer.ts'), `
28import type { CraftErrorCode, CraftError, NativeBridgeErrorCode } from './craft'
29const native: NativeBridgeErrorCode[] = ['BUSY', 'WEBVIEW_HANDLE_NOT_SET', 'INVALID_PARAMETER', 'NATIVE_CALL_FAILED', 'CAPABILITY_DISABLED']
30const compatible: CraftErrorCode[] = [...native, 'INVALID_PARAMS', 'NOT_AVAILABLE', 'STORAGE_FULL']
31const error: CraftError = { code: 'BUSY', message: 'Try later', timestamp: 0 }
32// @ts-expect-error Misspelled native codes must not widen the public union.
33const typo: CraftErrorCode = 'INVALID_PARAMTER'
34// @ts-expect-error A legacy mobile name is not a canonical native wire code.
35const legacy: NativeBridgeErrorCode = 'INVALID_PARAMS'
36void [compatible, error, typo, legacy]
37`)
38 writeFileSync(join(root, 'tsconfig.json'), JSON.stringify({ compilerOptions: { strict: true, noEmit: true, module: 'esnext', moduleResolution: 'bundler', lib: ['es2022', 'dom'], types: [] }, files: ['consumer.ts'] }))
39 const check = Bun.spawnSync([process.execPath, join(sdk, 'scripts/tsc.ts'), '-p', join(root, 'tsconfig.json')], { cwd: root, stdout: 'pipe', stderr: 'pipe' })
40 expect(check.stdout.toString() + check.stderr.toString()).toBe('')
41 expect(check.exitCode).toBe(0)
42 }
43 finally { rmSync(root, { recursive: true, force: true }) }
44})
packages/typescript/types/craft.d.tsmodified+25-1
Changes to packages/typescript/types/craft.d.ts
@@ -980,7 +980,31 @@ export interface DebugReport {
980980 timestamp: number;
981981}
982982
983export type CraftErrorCode =
983/**
984 * Canonical Zig bridge wire codes. Kept in parity with
985 * packages/zig/src/bridge_error.zig:errorCodeString by the SDK test suite.
986 */
987export type NativeBridgeErrorCode =
988 | 'WINDOW_HANDLE_NOT_SET'
989 | 'WEBVIEW_HANDLE_NOT_SET'
990 | 'TRAY_HANDLE_NOT_SET'
991 | 'UNKNOWN_ACTION'
992 | 'MISSING_DATA'
993 | 'INVALID_JSON'
994 | 'INVALID_PARAMETER'
995 | 'BUSY'
996 | 'PLATFORM_NOT_SUPPORTED'
997 | 'NATIVE_CALL_FAILED'
998 | 'ALLOCATION_FAILED'
999 | 'CANCELLED'
1000 | 'NOT_FOUND'
1001 | 'PERMISSION_DENIED'
1002 | 'TIMEOUT'
1003 | 'UNSAFE_COMMAND'
1004 | 'CAPABILITY_DISABLED';
1005
1006/** Native wire codes plus existing mobile/shim names retained for compatibility. */
1007export type CraftErrorCode = NativeBridgeErrorCode
9841008 | 'CRAFT_ERROR' // Generic error
9851009 | 'PERMISSION_DENIED' // Permission not granted
9861010 | 'NOT_AVAILABLE' // Feature not available on device