@chipmobilesdk/rn-camera 0.2.0 → 0.3.0

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.
@@ -1,120 +1,120 @@
1
- /**
2
- * Capture guards, single-flight enforcement, and metadata assembly
3
- * (FR-020..FR-026b).
4
- *
5
- * The single-flight guard rejects rather than queues or coalesces. A queue
6
- * turns a double-tapped shutter into two photos the user never asked for, and
7
- * coalescing hides a deliberate second capture behind an accidental one — so a
8
- * fast, explicit `captureInProgress` error is the only option that lets the app
9
- * decide (clarification session 2026-07-26).
10
- */
11
-
12
- import {
13
- known,
14
- unavailable,
15
- type CameraPosition,
16
- type CapabilityProfile,
17
- type CapturedImage,
18
- type CaptureRequest,
19
- type FlashMode,
20
- type Maybe,
21
- type SessionStatus,
22
- } from '../config/types';
23
- import { CameraError } from '../diagnostics/errors';
24
- import type { CameraEngine } from '../native/cameraEngine';
25
- import { assertSupported, supportsFlashMode, supportsPosition } from './capabilities';
26
- import { canCapture } from './sessionMachine';
27
-
28
- export const DEFAULT_QUALITY = 0.9;
29
-
30
- export function clampQuality(quality: number | undefined): number {
31
- if (quality === undefined || Number.isNaN(quality)) {
32
- return DEFAULT_QUALITY;
33
- }
34
- return Math.min(1, Math.max(0, quality));
35
- }
36
-
37
- export function clampMaxDimension(maxDimension: number | undefined): Maybe<number> {
38
- if (maxDimension === undefined || !Number.isFinite(maxDimension) || maxDimension <= 0) {
39
- return unavailable();
40
- }
41
- return known(Math.floor(maxDimension));
42
- }
43
-
44
- export interface CaptureContext {
45
- readonly engine: CameraEngine;
46
- readonly sessionStatus: SessionStatus;
47
- readonly permissionGranted: boolean;
48
- readonly capabilities: CapabilityProfile | null;
49
- readonly cameraPosition: CameraPosition;
50
- readonly flashMode: FlashMode;
51
- }
52
-
53
- /**
54
- * Validates a capture request against permission, readiness, and capability.
55
- * Throws before any artifact could be created, so a rejected capture never
56
- * leaves a file behind (FR-020).
57
- */
58
- export function assertCaptureAllowed(context: CaptureContext, request: CaptureRequest): void {
59
- if (!context.permissionGranted) {
60
- throw new CameraError('permissionDenied', 'Camera permission is not granted');
61
- }
62
- if (!canCapture(context.sessionStatus)) {
63
- throw new CameraError('notReady', `Camera is not ready (session: ${context.sessionStatus})`);
64
- }
65
- const flashMode = request.flashMode ?? context.flashMode;
66
- assertSupported(supportsFlashMode(context.capabilities, flashMode), `flash mode "${flashMode}"`);
67
-
68
- const position = request.cameraPosition ?? context.cameraPosition;
69
- assertSupported(supportsPosition(context.capabilities, position), `${position} camera`);
70
- }
71
-
72
- /**
73
- * Normalizes whatever the engine returned into the published contract, filling
74
- * in effective hint values and marking undeterminable fields explicitly
75
- * unavailable rather than guessing them (FR-022, FR-023).
76
- */
77
- export function finalizeResult(
78
- raw: CapturedImage,
79
- request: CaptureRequest,
80
- context: CaptureContext,
81
- ): CapturedImage {
82
- const ownership = request.destination ? 'appOwned' : 'packageTemporary';
83
- return {
84
- ...raw,
85
- ownership,
86
- cameraPosition: raw.cameraPosition ?? request.cameraPosition ?? context.cameraPosition,
87
- timestamp: raw.timestamp ?? Date.now(),
88
- effectiveQuality: raw.effectiveQuality ?? clampQuality(request.quality),
89
- effectiveMaxDimension: raw.effectiveMaxDimension ?? clampMaxDimension(request.maxDimension),
90
- fileSize: raw.fileSize ?? unavailable(),
91
- orientation: raw.orientation ?? unavailable(),
92
- };
93
- }
94
-
95
- /**
96
- * Serializes captures. The slot is taken synchronously, before the operation
97
- * runs, so two calls in the same tick cannot both pass the check — and the
98
- * operation's own first `emit()` already sees `busy`. Holding the operation's
99
- * promise instead set the slot only after that first emit, so `capturing: true`
100
- * was never published at all.
101
- */
102
- export class SingleFlight {
103
- private active = false;
104
-
105
- get busy(): boolean {
106
- return this.active;
107
- }
108
-
109
- async run(operation: () => Promise<CapturedImage>): Promise<CapturedImage> {
110
- if (this.active) {
111
- throw new CameraError('captureInProgress', 'A capture is already in progress');
112
- }
113
- this.active = true;
114
- try {
115
- return await operation();
116
- } finally {
117
- this.active = false;
118
- }
119
- }
120
- }
1
+ /**
2
+ * Capture guards, single-flight enforcement, and metadata assembly
3
+ * (FR-020..FR-026b).
4
+ *
5
+ * The single-flight guard rejects rather than queues or coalesces. A queue
6
+ * turns a double-tapped shutter into two photos the user never asked for, and
7
+ * coalescing hides a deliberate second capture behind an accidental one — so a
8
+ * fast, explicit `captureInProgress` error is the only option that lets the app
9
+ * decide (clarification session 2026-07-26).
10
+ */
11
+
12
+ import {
13
+ known,
14
+ unavailable,
15
+ type CameraPosition,
16
+ type CapabilityProfile,
17
+ type CapturedImage,
18
+ type CaptureRequest,
19
+ type FlashMode,
20
+ type Maybe,
21
+ type SessionStatus,
22
+ } from '../config/types';
23
+ import { CameraError } from '../diagnostics/errors';
24
+ import type { CameraEngine } from '../native/cameraEngine';
25
+ import { assertSupported, supportsFlashMode, supportsPosition } from './capabilities';
26
+ import { canCapture } from './sessionMachine';
27
+
28
+ export const DEFAULT_QUALITY = 0.9;
29
+
30
+ export function clampQuality(quality: number | undefined): number {
31
+ if (quality === undefined || Number.isNaN(quality)) {
32
+ return DEFAULT_QUALITY;
33
+ }
34
+ return Math.min(1, Math.max(0, quality));
35
+ }
36
+
37
+ export function clampMaxDimension(maxDimension: number | undefined): Maybe<number> {
38
+ if (maxDimension === undefined || !Number.isFinite(maxDimension) || maxDimension <= 0) {
39
+ return unavailable();
40
+ }
41
+ return known(Math.floor(maxDimension));
42
+ }
43
+
44
+ export interface CaptureContext {
45
+ readonly engine: CameraEngine;
46
+ readonly sessionStatus: SessionStatus;
47
+ readonly permissionGranted: boolean;
48
+ readonly capabilities: CapabilityProfile | null;
49
+ readonly cameraPosition: CameraPosition;
50
+ readonly flashMode: FlashMode;
51
+ }
52
+
53
+ /**
54
+ * Validates a capture request against permission, readiness, and capability.
55
+ * Throws before any artifact could be created, so a rejected capture never
56
+ * leaves a file behind (FR-020).
57
+ */
58
+ export function assertCaptureAllowed(context: CaptureContext, request: CaptureRequest): void {
59
+ if (!context.permissionGranted) {
60
+ throw new CameraError('permissionDenied', 'Camera permission is not granted');
61
+ }
62
+ if (!canCapture(context.sessionStatus)) {
63
+ throw new CameraError('notReady', `Camera is not ready (session: ${context.sessionStatus})`);
64
+ }
65
+ const flashMode = request.flashMode ?? context.flashMode;
66
+ assertSupported(supportsFlashMode(context.capabilities, flashMode), `flash mode "${flashMode}"`);
67
+
68
+ const position = request.cameraPosition ?? context.cameraPosition;
69
+ assertSupported(supportsPosition(context.capabilities, position), `${position} camera`);
70
+ }
71
+
72
+ /**
73
+ * Normalizes whatever the engine returned into the published contract, filling
74
+ * in effective hint values and marking undeterminable fields explicitly
75
+ * unavailable rather than guessing them (FR-022, FR-023).
76
+ */
77
+ export function finalizeResult(
78
+ raw: CapturedImage,
79
+ request: CaptureRequest,
80
+ context: CaptureContext,
81
+ ): CapturedImage {
82
+ const ownership = request.destination ? 'appOwned' : 'packageTemporary';
83
+ return {
84
+ ...raw,
85
+ ownership,
86
+ cameraPosition: raw.cameraPosition ?? request.cameraPosition ?? context.cameraPosition,
87
+ timestamp: raw.timestamp ?? Date.now(),
88
+ effectiveQuality: raw.effectiveQuality ?? clampQuality(request.quality),
89
+ effectiveMaxDimension: raw.effectiveMaxDimension ?? clampMaxDimension(request.maxDimension),
90
+ fileSize: raw.fileSize ?? unavailable(),
91
+ orientation: raw.orientation ?? unavailable(),
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Serializes captures. The slot is taken synchronously, before the operation
97
+ * runs, so two calls in the same tick cannot both pass the check — and the
98
+ * operation's own first `emit()` already sees `busy`. Holding the operation's
99
+ * promise instead set the slot only after that first emit, so `capturing: true`
100
+ * was never published at all.
101
+ */
102
+ export class SingleFlight {
103
+ private active = false;
104
+
105
+ get busy(): boolean {
106
+ return this.active;
107
+ }
108
+
109
+ async run(operation: () => Promise<CapturedImage>): Promise<CapturedImage> {
110
+ if (this.active) {
111
+ throw new CameraError('captureInProgress', 'A capture is already in progress');
112
+ }
113
+ this.active = true;
114
+ try {
115
+ return await operation();
116
+ } finally {
117
+ this.active = false;
118
+ }
119
+ }
120
+ }