@flighthq/haptics 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e

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/README.md CHANGED
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/haptics`. The `@flig
13
13
  This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
14
14
 
15
15
  - [Flight project](https://github.com/flighthq/flight)
16
- - [Source for @flighthq/haptics@0.5.0-next.1944.cd6b6e7](https://github.com/flighthq/flight/tree/cd6b6e71eb472b3a4249830a7743333f019fa7b7/packages/haptics)
17
- - [License](https://github.com/flighthq/flight/blob/cd6b6e71eb472b3a4249830a7743333f019fa7b7/LICENSE.md)
16
+ - [Source for @flighthq/haptics@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/haptics)
17
+ - [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
package/dist/haptics.d.ts CHANGED
@@ -1,20 +1,12 @@
1
- import type { BackendExplanation } from '@flighthq/types/contract';
2
- import type { HapticImpactStyle, HapticNotificationType, HapticsBackend, HapticsCapabilities } from '@flighthq/types/contract';
3
- export declare function cancelDeviceVibration(): boolean;
4
- export declare function createWebHapticsBackend(): HapticsBackend;
5
- export declare function explainHapticsBackend(): BackendExplanation;
6
- export declare function getHapticsBackend(): HapticsBackend;
7
- export declare function getHapticsCapabilities(out: HapticsCapabilities): HapticsCapabilities;
8
- export declare function installHapticsHostBackend(backend: HapticsBackend): void;
9
- export declare function isHapticsSupported(): boolean;
10
- export declare function observeHapticsHostResult(operation: string, succeeded: boolean): void;
11
- export declare function prepareHaptics(): void;
12
- export declare function resetHapticsBackendForTest(): void;
13
- export declare function setHapticsBackend(backend: HapticsBackend | null): void;
14
- export declare function triggerHapticImpact(style: HapticImpactStyle, intensity?: number): boolean;
15
- export declare function triggerHapticNotification(type: HapticNotificationType): boolean;
16
- export declare function triggerHapticSelection(): boolean;
17
- export declare function vibrateDevice(durationMs: number): boolean;
18
- export declare function vibrateDevicePattern(pattern: Readonly<number[]>): boolean;
19
- export declare function vibrateDeviceWaveform(timings: Readonly<number[]>, amplitudes: Readonly<number[]>, repeat?: number): boolean;
1
+ import type { HapticImpactStyle, HapticNotificationType, HapticsCapabilities, HasInputHaptics } from '@flighthq/types/contract';
2
+ export declare function cancelDeviceVibration(host: HasInputHaptics): boolean;
3
+ export declare function getHapticsCapabilities(host: HasInputHaptics, out: HapticsCapabilities): HapticsCapabilities;
4
+ export declare function isHapticsSupported(host: HasInputHaptics): boolean;
5
+ export declare function prepareHaptics(host: HasInputHaptics): void;
6
+ export declare function triggerHapticImpact(host: HasInputHaptics, style: HapticImpactStyle, intensity?: number): boolean;
7
+ export declare function triggerHapticNotification(host: HasInputHaptics, type: HapticNotificationType): boolean;
8
+ export declare function triggerHapticSelection(host: HasInputHaptics): boolean;
9
+ export declare function vibrateDevice(host: HasInputHaptics, durationMs: number): boolean;
10
+ export declare function vibrateDevicePattern(host: HasInputHaptics, pattern: Readonly<number[]>): boolean;
11
+ export declare function vibrateDeviceWaveform(host: HasInputHaptics, timings: Readonly<number[]>, amplitudes: Readonly<number[]>, repeat?: number): boolean;
20
12
  //# sourceMappingURL=haptics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"haptics.d.ts","sourceRoot":"","sources":["../src/haptics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EACV,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,EACd,mBAAmB,EACpB,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAKD,wBAAgB,uBAAuB,IAAI,cAAc,CA2CxD;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAa1D;AAED,wBAAgB,iBAAiB,IAAI,cAAc,CAElD;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,mBAAmB,GAAG,mBAAmB,CAEpF;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAMvE;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAKpF;AAED,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAKjD;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAEtE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAEzF;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAE/E;AAED,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAGzE;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAC3B,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAC9B,MAAM,SAAK,GACV,OAAO,CAOT"}
1
+ {"version":3,"file":"haptics.d.ts","sourceRoot":"","sources":["../src/haptics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAEpE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,mBAAmB,GAAG,mBAAmB,CAE3G;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAEjE;AAID,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAE1D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAEhH;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAEtG;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAErE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAEhF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAGhG;AAID,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAC3B,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAC9B,MAAM,SAAK,GACV,OAAO,CAOT"}
package/dist/haptics.js CHANGED
@@ -1,123 +1,38 @@
1
- export function cancelDeviceVibration() {
2
- return getHapticsBackend().cancel();
1
+ export function cancelDeviceVibration(host) {
2
+ return host.input.haptics.cancel();
3
3
  }
4
- // Builds the default web backend over navigator.vibrate. Every method returns false when the Vibration
5
- // API is absent (jsdom, desktop browsers) or the call fails, rather than throwing. Web vibration is a
6
- // coarse approximation of native haptics: it can only buzz the motor for a duration/pattern.
7
- export function createWebHapticsBackend() {
8
- return {
9
- cancel() {
10
- return _webVibrate(0, 'cancel');
11
- },
12
- capabilities(out) {
13
- const supported = typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';
14
- out.amplitudeControl = false;
15
- out.customEvents = false;
16
- out.intensity = false;
17
- out.patterns = supported;
18
- out.supported = supported;
19
- return out;
20
- },
21
- impact(style, intensity) {
22
- const base = style === 'heavy' || style === 'rigid' ? 30 : style === 'medium' ? 20 : style === 'soft' ? 25 : 10;
23
- const ms = intensity !== undefined ? Math.round(base * Math.max(0, Math.min(1, intensity))) : base;
24
- return _webVibrate(ms, 'impact');
25
- },
26
- isSupported() {
27
- return typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';
28
- },
29
- notification(type) {
30
- const pattern = type === 'error' ? [20, 60, 20] : type === 'warning' ? [20, 60, 20, 60] : [15, 50, 15];
31
- return _webVibrate(pattern, 'notification');
32
- },
33
- prepare() { },
34
- selection() {
35
- return _webVibrate(5, 'selection');
36
- },
37
- vibrate(durationMs) {
38
- return _webVibrate(durationMs, 'vibrate');
39
- },
40
- vibratePattern(pattern) {
41
- if (pattern.length === 0)
42
- return false;
43
- return _webVibrate(pattern, 'vibratePattern');
44
- },
45
- vibrateWaveform(timings, _amplitudes, repeat) {
46
- if (timings.length === 0)
47
- return false;
48
- void repeat;
49
- return _webVibrate(timings, 'vibrateWaveform');
50
- },
51
- };
4
+ export function getHapticsCapabilities(host, out) {
5
+ return host.input.haptics.capabilities(out);
52
6
  }
53
- export function explainHapticsBackend() {
54
- if (_custom !== null) {
55
- return { conflict: _hostConflict, layer: 'custom', operation: null, viability: 'unobserved' };
56
- }
57
- if (_host !== null) {
58
- return {
59
- conflict: _hostConflict,
60
- layer: 'host',
61
- operation: _hostObservation !== null ? _hostObservation.operation : null,
62
- viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
63
- };
64
- }
65
- return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
66
- }
67
- export function getHapticsBackend() {
68
- return _custom ?? _host ?? _sentinel;
69
- }
70
- export function getHapticsCapabilities(out) {
71
- return getHapticsBackend().capabilities(out);
72
- }
73
- export function installHapticsHostBackend(backend) {
74
- if (_host !== null) {
75
- if (_host !== backend)
76
- _hostConflict = true;
77
- return;
78
- }
79
- _host = backend;
7
+ export function isHapticsSupported(host) {
8
+ return host.input.haptics.isSupported();
80
9
  }
81
- export function isHapticsSupported() {
82
- return getHapticsBackend().isSupported();
10
+ // Warm-up hint. `prepare` is optional on the backend, so a host whose provider does not pre-allocate
11
+ // simply does nothing here — that is the absence of a capability, not a failure.
12
+ export function prepareHaptics(host) {
13
+ host.input.haptics.prepare?.();
83
14
  }
84
- export function observeHapticsHostResult(operation, succeeded) {
85
- _hostObservation = {
86
- operation,
87
- viability: succeeded ? 'available' : 'runtime-api-unavailable',
88
- };
15
+ export function triggerHapticImpact(host, style, intensity) {
16
+ return host.input.haptics.impact(style, intensity ?? 1);
89
17
  }
90
- export function prepareHaptics() {
91
- getHapticsBackend().prepare?.();
18
+ export function triggerHapticNotification(host, type) {
19
+ return host.input.haptics.notification(type);
92
20
  }
93
- export function resetHapticsBackendForTest() {
94
- _custom = null;
95
- _host = null;
96
- _hostConflict = false;
97
- _hostObservation = null;
21
+ export function triggerHapticSelection(host) {
22
+ return host.input.haptics.selection();
98
23
  }
99
- export function setHapticsBackend(backend) {
100
- _custom = backend;
24
+ export function vibrateDevice(host, durationMs) {
25
+ return host.input.haptics.vibrate(durationMs);
101
26
  }
102
- export function triggerHapticImpact(style, intensity) {
103
- return getHapticsBackend().impact(style, intensity ?? 1);
104
- }
105
- export function triggerHapticNotification(type) {
106
- return getHapticsBackend().notification(type);
107
- }
108
- export function triggerHapticSelection() {
109
- return getHapticsBackend().selection();
110
- }
111
- export function vibrateDevice(durationMs) {
112
- return getHapticsBackend().vibrate(durationMs);
113
- }
114
- export function vibrateDevicePattern(pattern) {
27
+ export function vibrateDevicePattern(host, pattern) {
115
28
  if (pattern.length === 0)
116
29
  return false;
117
- return getHapticsBackend().vibratePattern(pattern);
30
+ return host.input.haptics.vibratePattern(pattern);
118
31
  }
119
- export function vibrateDeviceWaveform(timings, amplitudes, repeat = -1) {
120
- const backend = getHapticsBackend();
32
+ // Amplitude-aware waveform, falling back to a plain timing pattern when the selected provider does not
33
+ // implement one. The fallback drops amplitudes rather than failing: every backend can express timings.
34
+ export function vibrateDeviceWaveform(host, timings, amplitudes, repeat = -1) {
35
+ const backend = host.input.haptics;
121
36
  if (timings.length === 0)
122
37
  return false;
123
38
  if (backend.vibrateWaveform !== undefined) {
@@ -125,55 +40,4 @@ export function vibrateDeviceWaveform(timings, amplitudes, repeat = -1) {
125
40
  }
126
41
  return backend.vibratePattern(timings);
127
42
  }
128
- let _custom = null;
129
- let _host = null;
130
- let _hostConflict = false;
131
- let _hostObservation = null;
132
- const _sentinel = {
133
- cancel() {
134
- return false;
135
- },
136
- capabilities(out) {
137
- out.amplitudeControl = false;
138
- out.customEvents = false;
139
- out.intensity = false;
140
- out.patterns = false;
141
- out.supported = false;
142
- return out;
143
- },
144
- impact() {
145
- return false;
146
- },
147
- isSupported() {
148
- return false;
149
- },
150
- notification() {
151
- return false;
152
- },
153
- prepare() { },
154
- selection() {
155
- return false;
156
- },
157
- vibrate() {
158
- return false;
159
- },
160
- vibratePattern() {
161
- return false;
162
- },
163
- };
164
- function _webVibrate(pattern, operation) {
165
- if (typeof navigator === 'undefined' || !('vibrate' in navigator) || typeof navigator.vibrate !== 'function') {
166
- observeHapticsHostResult(operation, false);
167
- return false;
168
- }
169
- try {
170
- const result = navigator.vibrate(pattern);
171
- observeHapticsHostResult(operation, result);
172
- return result;
173
- }
174
- catch {
175
- observeHapticsHostResult(operation, false);
176
- return false;
177
- }
178
- }
179
43
  //# sourceMappingURL=haptics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"haptics.js","sourceRoot":"","sources":["../src/haptics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"haptics.js","sourceRoot":"","sources":["../src/haptics.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,qBAAqB,CAAC,IAAqB;IACzD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAqB,EAAE,GAAwB;IACpF,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAqB;IACtD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;AAC1C,CAAC;AAED,qGAAqG;AACrG,iFAAiF;AACjF,MAAM,UAAU,cAAc,CAAC,IAAqB;IAClD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAqB,EAAE,KAAwB,EAAE,SAAkB;IACrG,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,IAAI,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,IAAqB,EAAE,IAA4B;IAC3F,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAqB;IAC1D,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAqB,EAAE,UAAkB;IACrE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAqB,EAAE,OAA2B;IACrF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;AACpD,CAAC;AAED,uGAAuG;AACvG,uGAAuG;AACvG,MAAM,UAAU,qBAAqB,CACnC,IAAqB,EACrB,OAA2B,EAC3B,UAA8B,EAC9B,MAAM,GAAG,CAAC,CAAC;IAEX,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;IACnC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,OAAO,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,OAAO,CAAC,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;AACzC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { explainHapticsBackend, cancelDeviceVibration, getHapticsCapabilities, isHapticsSupported, prepareHaptics, triggerHapticImpact, triggerHapticNotification, triggerHapticSelection, vibrateDevice, vibrateDevicePattern, vibrateDeviceWaveform, } from './contract';
1
+ export { cancelDeviceVibration, getHapticsCapabilities, isHapticsSupported, prepareHaptics, triggerHapticImpact, triggerHapticNotification, triggerHapticSelection, vibrateDevice, vibrateDevicePattern, vibrateDeviceWaveform, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { explainHapticsBackend, cancelDeviceVibration, getHapticsCapabilities, isHapticsSupported, prepareHaptics, triggerHapticImpact, triggerHapticNotification, triggerHapticSelection, vibrateDevice, vibrateDevicePattern, vibrateDeviceWaveform, } from './contract';
1
+ export { cancelDeviceVibration, getHapticsCapabilities, isHapticsSupported, prepareHaptics, triggerHapticImpact, triggerHapticNotification, triggerHapticSelection, vibrateDevice, vibrateDevicePattern, vibrateDeviceWaveform, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/haptics",
3
- "version": "0.5.0-next.1944.cd6b6e7",
3
+ "version": "0.5.1-edge.638.d4dbd4e",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,7 +38,7 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/types": "0.5.0-next.1944.cd6b6e7"
41
+ "@flighthq/types": "0.5.1-edge.638.d4dbd4e"
42
42
  },
43
43
  "devDependencies": {
44
44
  "typescript": "^5.3.0"
@@ -3,28 +3,27 @@ import type {
3
3
  HapticNotificationType,
4
4
  HapticsBackend,
5
5
  HapticsCapabilities,
6
+ HasInputHaptics,
6
7
  } from '@flighthq/types/contract';
7
8
 
8
9
  import {
9
10
  cancelDeviceVibration,
10
- createWebHapticsBackend,
11
- getHapticsBackend,
12
11
  getHapticsCapabilities,
13
12
  isHapticsSupported,
14
13
  prepareHaptics,
15
- setHapticsBackend,
16
14
  triggerHapticImpact,
17
15
  triggerHapticNotification,
18
16
  triggerHapticSelection,
19
17
  vibrateDevice,
20
18
  vibrateDevicePattern,
21
19
  vibrateDeviceWaveform,
22
- explainHapticsBackend,
23
- installHapticsHostBackend,
24
- observeHapticsHostResult,
25
- resetHapticsBackendForTest,
26
20
  } from './haptics';
27
21
 
22
+ interface RecordedCall {
23
+ readonly args: readonly unknown[];
24
+ readonly name: string;
25
+ }
26
+
28
27
  function makeCapabilities(overrides: Partial<HapticsCapabilities> = {}): HapticsCapabilities {
29
28
  return {
30
29
  amplitudeControl: false,
@@ -36,391 +35,192 @@ function makeCapabilities(overrides: Partial<HapticsCapabilities> = {}): Haptics
36
35
  };
37
36
  }
38
37
 
39
- function fakeBackend(): HapticsBackend & {
40
- cancelCount: number;
41
- duration: number;
42
- lastPattern: Readonly<number[]> | null;
43
- lastWaveform: { amplitudes: Readonly<number[]>; repeat: number | undefined; timings: Readonly<number[]> } | null;
44
- prepared: number;
45
- style: HapticImpactStyle | null;
46
- styleIntensity: number | undefined;
47
- type: HapticNotificationType | null;
48
- selections: number;
49
- } {
50
- return {
51
- cancelCount: 0,
52
- duration: -1,
53
- lastPattern: null,
54
- lastWaveform: null,
55
- prepared: 0,
56
- style: null,
57
- styleIntensity: undefined,
58
- type: null,
59
- selections: 0,
60
- cancel() {
61
- this.cancelCount += 1;
62
- return true;
63
- },
38
+ // A host carrying a recording haptics provider. Every operation is explicit here — there is no ambient
39
+ // slot to install into and nothing to reset between tests, which is the point of the migration: two
40
+ // tests can hold two different providers at once without interfering.
41
+ function makeHost(overrides: Partial<HapticsBackend> = {}): { calls: RecordedCall[]; host: HasInputHaptics } {
42
+ const calls: RecordedCall[] = [];
43
+ const record =
44
+ (name: string, result: boolean) =>
45
+ (...args: unknown[]): boolean => {
46
+ calls.push({ args, name });
47
+ return result;
48
+ };
49
+ const haptics: HapticsBackend = {
50
+ cancel: record('cancel', true),
64
51
  capabilities(out: HapticsCapabilities): HapticsCapabilities {
65
- out.amplitudeControl = true;
66
- out.customEvents = false;
67
- out.intensity = true;
52
+ calls.push({ args: [], name: 'capabilities' });
68
53
  out.patterns = true;
69
54
  out.supported = true;
70
55
  return out;
71
56
  },
72
- impact(style: HapticImpactStyle, intensity?: number) {
73
- this.style = style;
74
- this.styleIntensity = intensity;
75
- return true;
76
- },
77
- isSupported() {
78
- return true;
79
- },
80
- notification(type: HapticNotificationType) {
81
- this.type = type;
82
- return true;
83
- },
84
- prepare() {
85
- this.prepared += 1;
86
- },
87
- selection() {
88
- this.selections += 1;
89
- return true;
90
- },
91
- vibrate(durationMs: number) {
92
- this.duration = durationMs;
93
- return true;
94
- },
95
- vibratePattern(pattern: Readonly<number[]>) {
96
- this.lastPattern = pattern;
97
- return true;
98
- },
99
- vibrateWaveform(timings: Readonly<number[]>, amplitudes: Readonly<number[]>, repeat?: number) {
100
- this.lastWaveform = { amplitudes, repeat, timings };
101
- return true;
102
- },
57
+ impact: record('impact', true) as HapticsBackend['impact'],
58
+ isSupported: record('isSupported', true) as HapticsBackend['isSupported'],
59
+ notification: record('notification', true) as HapticsBackend['notification'],
60
+ selection: record('selection', true) as HapticsBackend['selection'],
61
+ vibrate: record('vibrate', true) as HapticsBackend['vibrate'],
62
+ vibratePattern: record('vibratePattern', true) as HapticsBackend['vibratePattern'],
63
+ ...overrides,
103
64
  };
65
+ return { calls, host: { input: { haptics } } };
104
66
  }
105
67
 
106
- afterEach(() => setHapticsBackend(null));
107
-
108
68
  describe('cancelDeviceVibration', () => {
109
- it('forwards to backend cancel', () => {
110
- const backend = fakeBackend();
111
- setHapticsBackend(backend);
112
- expect(cancelDeviceVibration()).toBe(true);
113
- expect(backend.cancelCount).toBe(1);
114
- });
115
-
116
- it('returns false when haptics unavailable (jsdom)', () => {
117
- expect(cancelDeviceVibration()).toBe(false);
118
- });
119
- });
120
-
121
- describe('createWebHapticsBackend', () => {
122
- it('returns false for every method without throwing when vibrate is unavailable (jsdom)', () => {
123
- const backend = createWebHapticsBackend();
124
- expect(backend.vibrate(10)).toBe(false);
125
- expect(backend.impact('heavy')).toBe(false);
126
- expect(backend.impact('rigid')).toBe(false);
127
- expect(backend.impact('soft')).toBe(false);
128
- expect(backend.notification('error')).toBe(false);
129
- expect(backend.selection()).toBe(false);
130
- expect(backend.vibratePattern([100, 50, 100])).toBe(false);
131
- expect(backend.cancel()).toBe(false);
132
- expect(backend.isSupported()).toBe(false);
133
- });
134
-
135
- it('returns false for empty pattern without throwing', () => {
136
- const backend = createWebHapticsBackend();
137
- expect(backend.vibratePattern([])).toBe(false);
138
- });
139
-
140
- it('returns false for waveform with empty timings', () => {
141
- const backend = createWebHapticsBackend();
142
- expect(backend.vibrateWaveform!([], [], undefined)).toBe(false);
143
- });
144
-
145
- it('fills capabilities for web (no vibrate)', () => {
146
- const backend = createWebHapticsBackend();
147
- const out = makeCapabilities();
148
- backend.capabilities(out);
149
- expect(out.supported).toBe(false);
150
- expect(out.patterns).toBe(false);
151
- expect(out.intensity).toBe(false);
152
- expect(out.amplitudeControl).toBe(false);
153
- expect(out.customEvents).toBe(false);
154
- });
155
-
156
- it('prepare is a no-op and does not throw', () => {
157
- const backend = createWebHapticsBackend();
158
- expect(() => backend.prepare!()).not.toThrow();
159
- });
160
- });
161
-
162
- describe('explainHapticsBackend', () => {
163
- afterEach(() => resetHapticsBackendForTest());
164
-
165
- it('reports host-not-enabled when no backend is installed', () => {
166
- resetHapticsBackendForTest();
167
- const explanation = explainHapticsBackend();
168
- expect(explanation.layer).toBe('host-not-enabled');
169
- expect(explanation.conflict).toBe(false);
170
- expect(explanation.viability).toBe('unobserved');
171
- });
172
-
173
- it('reports custom layer when a custom backend is set', () => {
174
- setHapticsBackend(fakeBackend());
175
- expect(explainHapticsBackend().layer).toBe('custom');
176
- });
177
-
178
- it('reports host layer when a host backend is installed', () => {
179
- installHapticsHostBackend(fakeBackend());
180
- expect(explainHapticsBackend().layer).toBe('host');
69
+ it('forwards to the selected provider', () => {
70
+ const { calls, host } = makeHost();
71
+ expect(cancelDeviceVibration(host)).toBe(true);
72
+ expect(calls.map((call) => call.name)).toEqual(['cancel']);
181
73
  });
182
74
 
183
- it('reports conflict when two different host backends are installed', () => {
184
- installHapticsHostBackend(fakeBackend());
185
- installHapticsHostBackend(fakeBackend());
186
- expect(explainHapticsBackend().conflict).toBe(true);
187
- });
188
- });
189
-
190
- describe('getHapticsBackend', () => {
191
- it('falls back to a web backend', () => {
192
- expect(getHapticsBackend()).not.toBeNull();
193
- });
194
-
195
- it('returns the registered backend', () => {
196
- const backend = fakeBackend();
197
- setHapticsBackend(backend);
198
- expect(getHapticsBackend()).toBe(backend);
75
+ it('returns whatever the provider reports rather than assuming success', () => {
76
+ const { host } = makeHost({ cancel: () => false });
77
+ expect(cancelDeviceVibration(host)).toBe(false);
199
78
  });
200
79
  });
201
80
 
202
81
  describe('getHapticsCapabilities', () => {
203
- it('fills out via the fake backend', () => {
204
- const backend = fakeBackend();
205
- setHapticsBackend(backend);
82
+ it('fills and returns the caller-owned out parameter', () => {
83
+ const { host } = makeHost();
206
84
  const out = makeCapabilities();
207
- const result = getHapticsCapabilities(out);
208
- expect(result).toBe(out);
85
+ expect(getHapticsCapabilities(host, out)).toBe(out);
209
86
  expect(out.supported).toBe(true);
210
- expect(out.intensity).toBe(true);
211
87
  expect(out.patterns).toBe(true);
212
- expect(out.amplitudeControl).toBe(true);
213
- expect(out.customEvents).toBe(false);
214
88
  });
215
89
 
216
- it('fills out from web backend when no backend is set', () => {
217
- const out = makeCapabilities();
218
- getHapticsCapabilities(out);
219
- expect(out.supported).toBe(false);
220
- });
221
- });
222
-
223
- describe('installHapticsHostBackend', () => {
224
- afterEach(() => resetHapticsBackendForTest());
90
+ // ★ Two hosts, two providers, no interference. Under the ambient model the second install either
91
+ // overwrote the first or was refused as a conflict; there was exactly one answer per process.
92
+ it('reports each host independently when two providers are live at once', () => {
93
+ const supported = makeHost();
94
+ const unsupported = makeHost({
95
+ capabilities: (out: HapticsCapabilities): HapticsCapabilities => {
96
+ out.supported = false;
97
+ out.patterns = false;
98
+ return out;
99
+ },
100
+ });
225
101
 
226
- it('installs a host backend that getHapticsBackend returns', () => {
227
- const backend = fakeBackend();
228
- installHapticsHostBackend(backend);
229
- expect(getHapticsBackend()).toBe(backend);
230
- });
231
-
232
- it('is first-host-wins: a second different backend sets conflict', () => {
233
- const first = fakeBackend();
234
- const second = fakeBackend();
235
- installHapticsHostBackend(first);
236
- installHapticsHostBackend(second);
237
- expect(getHapticsBackend()).toBe(first);
238
- expect(explainHapticsBackend().conflict).toBe(true);
102
+ expect(getHapticsCapabilities(supported.host, makeCapabilities()).supported).toBe(true);
103
+ expect(getHapticsCapabilities(unsupported.host, makeCapabilities()).supported).toBe(false);
239
104
  });
240
105
  });
241
106
 
242
107
  describe('isHapticsSupported', () => {
243
- it('returns true from fake backend', () => {
244
- setHapticsBackend(fakeBackend());
245
- expect(isHapticsSupported()).toBe(true);
108
+ it('forwards to the selected provider', () => {
109
+ const { host } = makeHost();
110
+ expect(isHapticsSupported(host)).toBe(true);
246
111
  });
247
112
 
248
- it('returns false when vibrate is unavailable (jsdom)', () => {
249
- expect(isHapticsSupported()).toBe(false);
250
- });
251
- });
252
-
253
- describe('observeHapticsHostResult', () => {
254
- afterEach(() => resetHapticsBackendForTest());
255
-
256
- it('records a successful observation', () => {
257
- installHapticsHostBackend(fakeBackend());
258
- observeHapticsHostResult('cancel', true);
259
- const explanation = explainHapticsBackend();
260
- expect(explanation.operation).toBe('cancel');
261
- expect(explanation.viability).toBe('available');
262
- });
263
-
264
- it('records a failed observation', () => {
265
- installHapticsHostBackend(fakeBackend());
266
- observeHapticsHostResult('cancel', false);
267
- expect(explainHapticsBackend().viability).toBe('runtime-api-unavailable');
113
+ it('reports false for a provider that says so', () => {
114
+ const { host } = makeHost({ isSupported: () => false });
115
+ expect(isHapticsSupported(host)).toBe(false);
268
116
  });
269
117
  });
270
118
 
271
119
  describe('prepareHaptics', () => {
272
- it('calls prepare on backends that provide it', () => {
273
- const backend = fakeBackend();
274
- setHapticsBackend(backend);
275
- prepareHaptics();
276
- expect(backend.prepared).toBe(1);
120
+ it('calls prepare on providers that implement it', () => {
121
+ let prepared = false;
122
+ const { host } = makeHost({
123
+ prepare: (): void => {
124
+ prepared = true;
125
+ },
126
+ });
127
+ prepareHaptics(host);
128
+ expect(prepared).toBe(true);
277
129
  });
278
130
 
279
- it('does not throw when backend has no prepare method', () => {
280
- const backend = fakeBackend();
281
- // Simulate a backend with no optional prepare
282
- (backend as HapticsBackend & { prepare?: () => void }).prepare = undefined;
283
- setHapticsBackend(backend);
284
- expect(() => prepareHaptics()).not.toThrow();
285
- });
286
- });
287
-
288
- describe('resetHapticsBackendForTest', () => {
289
- it('clears all backend slots', () => {
290
- setHapticsBackend(fakeBackend());
291
- installHapticsHostBackend(fakeBackend());
292
- observeHapticsHostResult('cancel', true);
293
- resetHapticsBackendForTest();
294
- expect(explainHapticsBackend().layer).toBe('host-not-enabled');
295
- expect(explainHapticsBackend().conflict).toBe(false);
296
- expect(explainHapticsBackend().viability).toBe('unobserved');
297
- });
298
- });
299
-
300
- describe('setHapticsBackend', () => {
301
- it('clears back to the web fallback when passed null', () => {
302
- setHapticsBackend(fakeBackend());
303
- setHapticsBackend(null);
304
- expect(getHapticsBackend()).not.toBeNull();
131
+ // prepare is optional on HapticsBackend. A provider that does not pre-allocate is not a broken
132
+ // provider, so the absence has to be silent rather than a throw.
133
+ it('does nothing when the provider omits prepare', () => {
134
+ const { host } = makeHost();
135
+ expect(host.input.haptics.prepare).toBeUndefined();
136
+ expect(() => prepareHaptics(host)).not.toThrow();
305
137
  });
306
138
  });
307
139
 
308
140
  describe('triggerHapticImpact', () => {
309
- it('forwards style to the active backend and defaults intensity to 1', () => {
310
- const backend = fakeBackend();
311
- setHapticsBackend(backend);
312
- expect(triggerHapticImpact('medium')).toBe(true);
313
- expect(backend.style).toBe('medium');
314
- expect(backend.styleIntensity).toBe(1);
141
+ it('forwards the style and defaults intensity to 1', () => {
142
+ const { calls, host } = makeHost();
143
+ triggerHapticImpact(host, 'heavy');
144
+ expect(calls[0]).toEqual({ args: ['heavy', 1], name: 'impact' });
315
145
  });
316
146
 
317
- it('forwards intensity to the backend', () => {
318
- const backend = fakeBackend();
319
- setHapticsBackend(backend);
320
- triggerHapticImpact('heavy', 0.5);
321
- expect(backend.style).toBe('heavy');
322
- expect(backend.styleIntensity).toBe(0.5);
147
+ it('forwards an explicit intensity unchanged', () => {
148
+ const { calls, host } = makeHost();
149
+ triggerHapticImpact(host, 'light', 0.25);
150
+ expect(calls[0]).toEqual({ args: ['light', 0.25], name: 'impact' });
323
151
  });
324
152
 
325
- it('supports rigid and soft styles', () => {
326
- const backend = fakeBackend();
327
- setHapticsBackend(backend);
328
- triggerHapticImpact('rigid');
329
- expect(backend.style).toBe('rigid');
330
- triggerHapticImpact('soft');
331
- expect(backend.style).toBe('soft');
332
- });
333
-
334
- it('web backend clamps intensity to 0..1', () => {
335
- const webBackend = createWebHapticsBackend();
336
- // Should not throw on out-of-range intensity even if vibrate returns false
337
- expect(() => webBackend.impact('heavy', 2)).not.toThrow();
338
- expect(() => webBackend.impact('heavy', -0.5)).not.toThrow();
153
+ it('forwards every impact style', () => {
154
+ const styles: readonly HapticImpactStyle[] = ['heavy', 'light', 'medium', 'rigid', 'soft'];
155
+ const { calls, host } = makeHost();
156
+ for (const style of styles) triggerHapticImpact(host, style);
157
+ expect(calls.map((call) => call.args[0])).toEqual(styles);
339
158
  });
340
159
  });
341
160
 
342
161
  describe('triggerHapticNotification', () => {
343
- it('forwards type to the active backend', () => {
344
- const backend = fakeBackend();
345
- setHapticsBackend(backend);
346
- expect(triggerHapticNotification('success')).toBe(true);
347
- expect(backend.type).toBe('success');
162
+ it('forwards every notification type', () => {
163
+ const types: readonly HapticNotificationType[] = ['error', 'success', 'warning'];
164
+ const { calls, host } = makeHost();
165
+ for (const type of types) triggerHapticNotification(host, type);
166
+ expect(calls.map((call) => call.args[0])).toEqual(types);
348
167
  });
349
168
  });
350
169
 
351
170
  describe('triggerHapticSelection', () => {
352
- it('triggers a selection on the active backend', () => {
353
- const backend = fakeBackend();
354
- setHapticsBackend(backend);
355
- expect(triggerHapticSelection()).toBe(true);
356
- expect(backend.selections).toBe(1);
171
+ it('forwards to the selected provider', () => {
172
+ const { calls, host } = makeHost();
173
+ expect(triggerHapticSelection(host)).toBe(true);
174
+ expect(calls.map((call) => call.name)).toEqual(['selection']);
357
175
  });
358
176
  });
359
177
 
360
178
  describe('vibrateDevice', () => {
361
- it('forwards duration to the active backend', () => {
362
- const backend = fakeBackend();
363
- setHapticsBackend(backend);
364
- expect(vibrateDevice(42)).toBe(true);
365
- expect(backend.duration).toBe(42);
179
+ it('forwards the duration to the selected provider', () => {
180
+ const { calls, host } = makeHost();
181
+ vibrateDevice(host, 42);
182
+ expect(calls[0]).toEqual({ args: [42], name: 'vibrate' });
366
183
  });
367
184
  });
368
185
 
369
186
  describe('vibrateDevicePattern', () => {
370
- it('forwards pattern to the active backend', () => {
371
- const backend = fakeBackend();
372
- setHapticsBackend(backend);
373
- const pattern = [100, 50, 100] as const;
374
- expect(vibrateDevicePattern(pattern)).toBe(true);
375
- expect(backend.lastPattern).toEqual(pattern);
376
- });
377
-
378
- it('returns false for empty pattern', () => {
379
- const backend = fakeBackend();
380
- setHapticsBackend(backend);
381
- expect(vibrateDevicePattern([])).toBe(false);
187
+ it('forwards a non-empty pattern', () => {
188
+ const { calls, host } = makeHost();
189
+ vibrateDevicePattern(host, [10, 20]);
190
+ expect(calls[0]).toEqual({ args: [[10, 20]], name: 'vibratePattern' });
382
191
  });
383
192
 
384
- it('returns false when haptics unavailable (jsdom)', () => {
385
- expect(vibrateDevicePattern([100, 50, 100])).toBe(false);
193
+ it('rejects an empty pattern before reaching the provider', () => {
194
+ const { calls, host } = makeHost();
195
+ expect(vibrateDevicePattern(host, [])).toBe(false);
196
+ expect(calls).toEqual([]);
386
197
  });
387
198
  });
388
199
 
389
200
  describe('vibrateDeviceWaveform', () => {
390
- it('calls vibrateWaveform on backends that support it', () => {
391
- const backend = fakeBackend();
392
- setHapticsBackend(backend);
393
- const timings = [100, 50, 100] as const;
394
- const amplitudes = [255, 0, 128] as const;
395
- expect(vibrateDeviceWaveform(timings, amplitudes, 0)).toBe(true);
396
- expect(backend.lastWaveform).toEqual({ timings, amplitudes, repeat: 0 });
397
- });
398
-
399
- it('defaults repeat to -1', () => {
400
- const backend = fakeBackend();
401
- setHapticsBackend(backend);
402
- vibrateDeviceWaveform([100], [255]);
403
- expect(backend.lastWaveform?.repeat).toBe(-1);
404
- });
405
-
406
- it('falls back to vibratePattern when backend lacks vibrateWaveform', () => {
407
- const backend = fakeBackend();
408
- (backend as HapticsBackend).vibrateWaveform = undefined;
409
- setHapticsBackend(backend);
410
- const timings = [100, 50] as const;
411
- const amplitudes = [255, 0] as const;
412
- expect(vibrateDeviceWaveform(timings, amplitudes)).toBe(true);
413
- expect(backend.lastPattern).toEqual(timings);
414
- expect(backend.lastWaveform).toBeNull();
415
- });
416
-
417
- it('returns false for empty timings', () => {
418
- const backend = fakeBackend();
419
- setHapticsBackend(backend);
420
- expect(vibrateDeviceWaveform([], [], undefined)).toBe(false);
421
- });
422
-
423
- it('returns false when haptics unavailable (jsdom)', () => {
424
- expect(vibrateDeviceWaveform([100], [255])).toBe(false);
201
+ it('uses vibrateWaveform when the provider implements it', () => {
202
+ const { calls, host } = makeHost({
203
+ vibrateWaveform: (...args: unknown[]): boolean => {
204
+ calls.push({ args, name: 'vibrateWaveform' });
205
+ return true;
206
+ },
207
+ } as Partial<HapticsBackend>);
208
+ expect(vibrateDeviceWaveform(host, [10, 20], [255, 128], 1)).toBe(true);
209
+ expect(calls[0]).toEqual({ args: [[10, 20], [255, 128], 1], name: 'vibrateWaveform' });
210
+ });
211
+
212
+ // The fallback drops amplitudes rather than failing. Every provider can express timings, so a
213
+ // provider without amplitude support should still buzz rather than silently do nothing.
214
+ it('falls back to vibratePattern with the timings when vibrateWaveform is absent', () => {
215
+ const { calls, host } = makeHost();
216
+ expect(host.input.haptics.vibrateWaveform).toBeUndefined();
217
+ expect(vibrateDeviceWaveform(host, [10, 20], [255, 128])).toBe(true);
218
+ expect(calls[0]).toEqual({ args: [[10, 20]], name: 'vibratePattern' });
219
+ });
220
+
221
+ it('rejects empty timings before reaching either path', () => {
222
+ const { calls, host } = makeHost();
223
+ expect(vibrateDeviceWaveform(host, [], [])).toBe(false);
224
+ expect(calls).toEqual([]);
425
225
  });
426
226
  });