also looking at this
fix(types): declare canonical native bridge error codes
#304
3 files
+86
-1
| @@ -453,6 +453,23 @@ See [benchmarks](../../benchmarks) for detailed performance comparisons. | ||
| 453 | 453 | - **Zero dependencies** - Pure Node.js APIs only |
| 454 | 454 | - **Better DX** - Hot reload and dev tools built-in |
| 455 | 455 | |
| 456 | ## Native bridge error codes | |
| 457 | ||
| 458 | The mobile bridge declarations in `types/craft.d.ts` distinguish canonical | |
| 459 | `NativeBridgeErrorCode` values from the broader compatible `CraftErrorCode` union. | |
| 460 | The native wire vocabulary comes from `packages/zig/src/bridge_error.zig`; SDK | |
| 461 | parity tests fail if the declaration drifts. It includes `INVALID_PARAMETER`, | |
| 462 | `PLATFORM_NOT_SUPPORTED`, `NATIVE_CALL_FAILED`, `WEBVIEW_HANDLE_NOT_SET`, `BUSY`, | |
| 463 | and `CAPABILITY_DISABLED`. | |
| 464 | ||
| 465 | Existing mobile/shim names such as `INVALID_PARAMS` and `NOT_AVAILABLE` remain | |
| 466 | accepted by `CraftErrorCode` for source compatibility. They are not aliases that | |
| 467 | the SDK rewrites on the wire. Applications supporting multiple host versions | |
| 468 | should handle the codes those hosts actually return; new native error handling | |
| 469 | should use the canonical spellings. A capability disabled by app configuration | |
| 470 | is distinct from a user denying permission, and `BUSY` is distinct from a failed | |
| 471 | native call. | |
| 472 | ||
| 456 | 473 | ## Requirements |
| 457 | 474 | |
| 458 | 475 | - Bun >= 1.0.0 (for development) |
| @@ -0,0 +1,44 @@ | ||
| 1 | import { expect, test } from 'bun:test' | |
| 2 | import { copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' | |
| 3 | import { tmpdir } from 'node:os' | |
| 4 | import { join, resolve } from 'node:path' | |
| 5 | ||
| 6 | const sdk = resolve(import.meta.dir, '../..') | |
| 7 | const declarations = join(sdk, 'types/craft.d.ts') | |
| 8 | ||
| 9 | test('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 | ||
| 23 | test('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'), ` | |
| 28 | import type { CraftErrorCode, CraftError, NativeBridgeErrorCode } from './craft' | |
| 29 | const native: NativeBridgeErrorCode[] = ['BUSY', 'WEBVIEW_HANDLE_NOT_SET', 'INVALID_PARAMETER', 'NATIVE_CALL_FAILED', 'CAPABILITY_DISABLED'] | |
| 30 | const compatible: CraftErrorCode[] = [...native, 'INVALID_PARAMS', 'NOT_AVAILABLE', 'STORAGE_FULL'] | |
| 31 | const error: CraftError = { code: 'BUSY', message: 'Try later', timestamp: 0 } | |
| 32 | // @ts-expect-error Misspelled native codes must not widen the public union. | |
| 33 | const typo: CraftErrorCode = 'INVALID_PARAMTER' | |
| 34 | // @ts-expect-error A legacy mobile name is not a canonical native wire code. | |
| 35 | const legacy: NativeBridgeErrorCode = 'INVALID_PARAMS' | |
| 36 | void [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 | }) | |
| @@ -980,7 +980,31 @@ export interface DebugReport { | ||
| 980 | 980 | timestamp: number; |
| 981 | 981 | } |
| 982 | 982 | |
| 983 | export 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 | */ | |
| 987 | export 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. */ | |
| 1007 | export type CraftErrorCode = NativeBridgeErrorCode | |
| 984 | 1008 | | 'CRAFT_ERROR' // Generic error |
| 985 | 1009 | | 'PERMISSION_DENIED' // Permission not granted |
| 986 | 1010 | | 'NOT_AVAILABLE' // Feature not available on device |