[Bug]: ShapeSource.getClusterLeaves crash on iOS (NOBRIDGE mode)
Mapbox Implementation
Mapbox
Mapbox Version
11.3.0
React Native Version
0.75.2
Platform
iOS
@rnmapbox/maps version
10.1.31
Standalone component to reproduce
import React, {useRef} from 'react';
import {Button} from 'react-native';
import {
Images,
MapView,
ShapeSource,
SymbolLayer,
CircleLayer,
Camera,
} from '@rnmapbox/maps';
const styles = {
mapView: {width: '100%', height: 500},
cluster: {
circleRadiusTransition: {duration: 5000, delay: 0},
circleColor: '#ff0000',
},
};
const features = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
id: 'a-feature',
properties: {
icon: 'example',
},
geometry: {
type: 'Point',
coordinates: [-74.00597, 40.71427],
},
},
{
type: 'Feature',
id: 'b-feature',
properties: {
icon: 'example',
},
geometry: {
type: 'Point',
coordinates: [-74.001097, 40.71527],
},
},
{
type: 'Feature',
id: 'c-feature',
properties: {
icon: 'example',
},
geometry: {
type: 'Point',
coordinates: [-74.00697, 40.72427],
},
},
],
};
const BugReportExample = () => {
const mapView = useRef<MapView>(null);
const shapeSourceMarkers = useRef<ShapeSource>(null);
const getVisibleMarkers = async () => {
let visibleMarkers: any[] = [];
const arrFeatures = await mapView.current?.queryRenderedFeaturesInRect(
[],
[],
['marker', 'cluster'],
);
if (!arrFeatures) {
return visibleMarkers;
}
for (let index = 0; index < arrFeatures.features.length; index++) {
const feature = arrFeatures.features[index];
const properties = feature.properties;
if (!properties) {
continue;
}
if (properties.cluster) {
try {
// ====> THE FOLLOWING LINE CRASHES THE WHOLE APP <====
const collection = await shapeSourceMarkers.current?.getClusterLeaves(
feature,
properties.point_count,
0,
);
collection.features.forEach((f: any) => {
visibleMarkers.push(f);
});
} catch (error: any) {
console.debug('error', error);
}
} else {
visibleMarkers.push(feature);
}
}
console.debug('visibleMarkers', visibleMarkers);
return visibleMarkers;
};
const circleLayerStyle = {
...styles.cluster,
...{circleRadius: 15},
};
return (
<>
<MapView style={styles.mapView} ref={mapView}>
<Camera
defaultSettings={{
centerCoordinate: [-74.001097, 40.71527],
zoomLevel: 10,
}}
/>
<Images images={{example: {uri: 'https://27crags-sandbox.s3.amazonaws.com/v6-icon.png'}}} />
<ShapeSource
id={'shape-source-markers'}
ref={shapeSourceMarkers}
shape={features}
cluster={true}
clusterRadius={30}
clusterMaxZoomLevel={19}>
<SymbolLayer
id="marker"
style={{
iconImage: ['get', 'icon'],
iconAllowOverlap: false,
iconSize: 0.5,
}}
slot={'middle'}
filter={['!', ['has', 'point_count']]}
/>
<SymbolLayer
id="pointCount"
style={{
textField: ['format', ['concat', ['get', 'point_count']]],
textSize: 12,
textPitchAlignment: 'viewport',
textAllowOverlap: false,
}}
/>
<CircleLayer
id={'cluster'}
belowLayerID="pointCount"
style={circleLayerStyle}
slot={'bottom'}
filter={['has', 'point_count']}
/>
</ShapeSource>
</MapView>
<Button
title="Count all items inside clusters"
onPress={() => {
getVisibleMarkers().then(visibleMarkers => {
console.debug('visibleMarkers', visibleMarkers);
});
}}
/>
</>
);
};
export default BugReportExample;
Observed behavior and steps to reproduce
When trying to get clusterLeaves using the method getClusterLeaves() of ShapeSource the app crashes immediately.
0 libobjc.A.dylib 0x18005d44c objc_retain + 8
1 libobjc.A.dylib 0x1800856c0 objc_storeStrong + 44
2 RNLatest 0x101870b78 -[RNMBXShapeSourceModule getClusterLeaves:featureJSON:number:offset:resolve:reject:] + 120 (RNMBXShapeSourceModule.mm:56)
3 CoreFoundation 0x1804b4720 __invoking___ + 144
4 CoreFoundation 0x1804b1a84 -[NSInvocation invoke] + 276
5 CoreFoundation 0x1804b1d1c -[NSInvocation invokeWithTarget:] + 60
6 RNLatest 0x1012bc5c8 invocation function for block in facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*) + 240 (RCTTurboModule.mm:347)
7 RNLatest 0x1012d2d90 facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2::operator()() const + 96 (RCTTurboModule.mm:380)
8 RNLatest 0x1012d2d24 decltype(std::declval<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2&>()()) std::__1::__invoke[abi:ue170006]<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2&>(facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2&) + 24 (invoke.h:340)
9 RNLatest 0x1012d2cdc void std::__1::__invoke_void_return_wrapper<void, true>::__call[abi:ue170006]<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2&>(facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2&) + 24 (invoke.h:415)
10 RNLatest 0x1012d2cb8 std::__1::__function::__alloc_func<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2, std::__1::allocator<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2>, void ()>::operator()[abi:ue170006]() + 28 (function.h:193)
11 RNLatest 0x1012d1a1c std::__1::__function::__func<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2, std::__1::allocator<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2>, void ()>::operator()() + 28 (function.h:364)
12 RNLatest 0x100e7b534 std::__1::__function::__value_func<void ()>::operator()[abi:ue170006]() const + 68 (function.h:518)
13 RNLatest 0x100e7b454 std::__1::function<void ()>::operator()() const + 24 (function.h:1169)
14 RNLatest 0x1012e25e4 invocation function for block in (anonymous namespace)::ModuleNativeMethodCallInvoker::invokeAsync(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, std::__1::function<void ()>&&) + 44 (RCTTurboModuleManager.mm:129)
15 libdispatch.dylib 0x180170104 _dispatch_call_block_and_release + 24
16 libdispatch.dylib 0x180171978 _dispatch_client_callout + 16
17 libdispatch.dylib 0x180179b10 _dispatch_lane_serial_drain + 960
18 libdispatch.dylib 0x18017a688 _dispatch_lane_invoke + 388
19 libdispatch.dylib 0x180185a84 _dispatch_root_queue_drain_deferred_wlh + 276
20 libdispatch.dylib 0x1801850d0 _dispatch_workloop_worker_thread + 448
21 libsystem_pthread.dylib 0x1042cb814 _pthread_wqthread + 284
22 libsystem_pthread.dylib 0x1042ca5d4 start_wqthread + 8
Expected behavior
ShapeSource.getClusterLeaves() used to worked on old architecture.
Notes / preliminary analysis
No response
Additional links and references
https://github.com/user-attachments/assets/5a8f6a69-0eea-4e19-a3e4-eb01c5ced27b
This is happening to me as well.
https://github.com/user-attachments/assets/3b59ec62-a823-4c1e-8af5-c4349c14bf82
Here is the relevant crash report :
Thread 24 name:
Thread 24 Crashed:
0 libobjc.A.dylib 0x000000018382605c objc_retain + 8 (:-1)
1 MhMap 0x0000000102aab504 -[RNMBXShapeSourceModule getClusterLeaves:featureJSON:number:offset:resolve:reject:] + 72 (RNMBXShapeSourceModule.mm:56)
2 CoreFoundation 0x000000018b92d814 __invoking___ + 148 (:-1)
3 CoreFoundation 0x000000018b92c860 -[NSInvocation invoke] + 428 (NSForwarding.m:3411)
4 CoreFoundation 0x000000018b9a31dc -[NSInvocation invokeWithTarget:] + 64 (NSForwarding.m:3508)
5 MhMap 0x0000000102895254 invocation function for block in facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*) + 120 (RCTTurboModule.mm:347)
6 MhMap 0x000000010289a978 facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2::operator()() const + 68 (RCTTurboModule.mm:380)
7 MhMap 0x000000010289a978 decltype(std::declval<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2&>()()) std::__1::__invoke[abi:ue1700... + 68 (invoke.h:340)
8 MhMap 0x000000010289a978 void std::__1::__invoke_void_return_wrapper<void, true>::__call[abi:ue170006]<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NS... + 68 (invoke.h:415)
9 MhMap 0x000000010289a978 std::__1::__function::__alloc_func<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2, std::__1::allocator<fa... + 68 (function.h:193)
10 MhMap 0x000000010289a978 std::__1::__function::__func<facebook::react::ObjCTurboModule::performMethodInvocation(facebook::jsi::Runtime&, bool, char const*, NSInvocation*, NSMutableArray*)::$_2, std::__1::allocator<facebook... + 88 (function.h:364)
11 libdispatch.dylib 0x000000019383513c _dispatch_call_block_and_release + 32 (init.c:1530)
12 libdispatch.dylib 0x0000000193836dd4 _dispatch_client_callout + 20 (object.m:576)
13 libdispatch.dylib 0x000000019383e400 _dispatch_lane_serial_drain + 748 (queue.c:3900)
14 libdispatch.dylib 0x000000019383ef30 _dispatch_lane_invoke + 380 (queue.c:3991)
15 libdispatch.dylib 0x0000000193849cb4 _dispatch_root_queue_drain_deferred_wlh + 288 (queue.c:6998)
16 libdispatch.dylib 0x0000000193849528 _dispatch_workloop_worker_thread + 404 (queue.c:6592)
17 libsystem_pthread.dylib 0x00000001e83e0934 _pthread_wqthread + 288 (pthread.c:2696)
18 libsystem_pthread.dylib 0x00000001e83dd0cc start_wqthread + 8 (:-1)
For information, I did a bit more debugging.
Here are the related line portion in the bridge :
// RNMBXShapeSourceModule.mm:56
RCT_EXPORT_METHOD(getClusterLeaves:(nonnull NSNumber *)viewRef featureJSON:(NSString *)featureJSON number:(double)number offset:(double)offset resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
[self withShapeSource:viewRef block:^(RNMBXShapeSource *view) {
[RNMBXShapeSourceViewManager getClusterLeavesWithShapeSource:view featureJSON:featureJSON number:number offset:offset resolver:resolve rejecter:reject];
} reject:reject methodName:@"getClusterLeaves"];
}
I'm absolutely not competent in native iOS, here is the ChatGPT insight if that helps :
This crash log suggests an issue with memory management, particularly involving objc_retain in the context of the getClusterLeaves method. Here's a breakdown of the potential problem and solutions:
Problem Analysis
Crash Context
The crash happens in the objc_retain function, indicating an issue with object lifecycle management (e.g., an object being released prematurely or being incorrectly retained).
Error Trace
The issue is in the RNMBXShapeSourceModule method, specifically at [RNMBXShapeSourceModule getClusterLeaves:featureJSON:number:offset:resolve:reject:].
The block invocation in [self withShapeSource:viewRef block:] may not properly handle the lifecycle of the RNMBXShapeSource.
Underlying Causes
Null or Invalid Object: The view object passed to the block could be null or improperly managed.
Retain Cycle or Weak Reference: If the view object is weakly referenced but is deallocated before being used, the code might attempt to retain a dangling pointer.
TurboModule Interaction: TurboModule bindings might be passing incorrect or corrupted references to Objective-C objects, causing improper retain/release behavior.
same here!
temporary disabled new arch (RCT_NEW_ARCH_ENABLED=0 pod install)
Fixed it with a patch-package.
Edit RNMBXShapeSourceModule.mm and change
RCT_EXPORT_METHOD(getClusterLeaves:(nonnull NSNumber *)viewRef featureJSON:(NSString *)featureJSON number:(double)number offset:(double)offset resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
[self withShapeSource:viewRef block:^(RNMBXShapeSource *view) {
[RNMBXShapeSourceViewManager getClusterLeavesWithShapeSource:view featureJSON:featureJSON number:number offset:offset resolver:resolve rejecter:reject];
} reject:reject methodName:@"getClusterLeaves"];
}
to
RCT_EXPORT_METHOD(getClusterLeaves:(nonnull NSNumber *)viewRef featureJSON:(NSString *)featureJSON number:(long)number offset:(long)offset resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
[self withShapeSource:viewRef block:^(RNMBXShapeSource *view) {
[RNMBXShapeSourceViewManager getClusterLeavesWithShapeSource:view featureJSON:featureJSON number:number offset:offset resolver:resolve rejecter:reject];
} reject:reject methodName:@"getClusterLeaves"];
}
Then edit RNMBXShapeSourceViewManager.swift
And set the number value to 999. Adjust to your need. This is essentially hardcoding it since it's not reading correctly from the JS code. I don't have more energy to figure out why haha.
e.g. change
@objc public static func getClusterLeaves(
shapeSource: RNMBXShapeSource,
featureJSON: String,
number: uint,
offset: uint,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock) -> Void
{
shapeSource.getClusterLeaves(featureJSON, number: 999, offset: offset) { result in
switch result {
case .success(let features):
logged("getClusterLeaves", rejecter: rejecter) {
let featuresJSON : Any = try features.features.toJSON()
resolver([
"data": ["type":"FeatureCollection", "features": featuresJSON]
])
}
case .failure(let error):
rejecter(error.localizedDescription, "Error.getClusterLeaves", error)
}
}
}
to
@objc public static func getClusterLeaves(
shapeSource: RNMBXShapeSource,
featureJSON: String,
number: uint,
offset: uint,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock) -> Void
{
shapeSource.getClusterLeaves(featureJSON, number: number, offset: offset) { result in
switch result {
case .success(let features):
logged("getClusterLeaves", rejecter: rejecter) {
let featuresJSON : Any = try features.features.toJSON()
resolver([
"data": ["type":"FeatureCollection", "features": featuresJSON]
])
}
case .failure(let error):
rejecter(error.localizedDescription, "Error.getClusterLeaves", error)
}
}
}
Then create a patch-package so the change persists between npm installs:
yarn add patch-package postinstall-postinstall --dev
or
npm install patch-package postinstall-postinstall --save-dev
then
npx patch-package @rnmapbox/maps
thanks, @nomadnic, here is a patch file to 10.1.33 latest (at the moment) version
diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceModule.mm b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceModule.mm
index d779a6c..17a8c89 100644
--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceModule.mm
+++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceModule.mm
@@ -53,7 +53,7 @@ - (void)withShapeSource:(nonnull NSNumber*)viewRef block:(void (^)(RNMBXShapeSou
} reject:reject methodName:@"getClusterChildren"];
}
-RCT_EXPORT_METHOD(getClusterLeaves:(nonnull NSNumber *)viewRef featureJSON:(NSString *)featureJSON number:(double)number offset:(double)offset resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
+RCT_EXPORT_METHOD(getClusterLeaves:(nonnull NSNumber *)viewRef featureJSON:(NSString *)featureJSON number:(long)number offset:(long)offset resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
[self withShapeSource:viewRef block:^(RNMBXShapeSource *view) {
[RNMBXShapeSourceViewManager getClusterLeavesWithShapeSource:view featureJSON:featureJSON number:number offset:offset resolver:resolve rejecter:reject];
diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceViewManager.swift b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceViewManager.swift
index 84f0407..f768c98 100644
--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceViewManager.swift
+++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceViewManager.swift
@@ -30,28 +30,28 @@ extension RNMBXShapeSourceViewManager {
}
}
- @objc public static func getClusterLeaves(
- shapeSource: RNMBXShapeSource,
- featureJSON: String,
- number: uint,
- offset: uint,
- resolver: @escaping RCTPromiseResolveBlock,
- rejecter: @escaping RCTPromiseRejectBlock) -> Void
- {
- shapeSource.getClusterLeaves(featureJSON, number: number, offset: offset) { result in
- switch result {
- case .success(let features):
- logged("getClusterLeaves", rejecter: rejecter) {
- let featuresJSON : Any = try features.features.toJSON()
- resolver([
- "data": ["type":"FeatureCollection", "features": featuresJSON]
- ])
- }
- case .failure(let error):
- rejecter(error.localizedDescription, "Error.getClusterLeaves", error)
+ @objc public static func getClusterLeaves(
+ shapeSource: RNMBXShapeSource,
+ featureJSON: String,
+ number: uint,
+ offset: uint,
+ resolver: @escaping RCTPromiseResolveBlock,
+ rejecter: @escaping RCTPromiseRejectBlock) -> Void
+ {
+ shapeSource.getClusterLeaves(featureJSON, number: 999, offset: offset) { result in
+ switch result {
+ case .success(let features):
+ logged("getClusterLeaves", rejecter: rejecter) {
+ let featuresJSON : Any = try features.features.toJSON()
+ resolver([
+ "data": ["type":"FeatureCollection", "features": featuresJSON]
+ ])
+ }
+ case .failure(let error):
+ rejecter(error.localizedDescription, "Error.getClusterLeaves", error)
+ }
}
- }
- }
+ }
@objc public static func getClusterChildren(
shapeSource: RNMBXShapeSource,
```diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceModule.mm b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceModule.mm
index d779a6c..17a8c89 100644
--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceModule.mm
+++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceModule.mm
@@ -53,7 +53,7 @@ - (void)withShapeSource:(nonnull NSNumber*)viewRef block:(void (^)(RNMBXShapeSou
} reject:reject methodName:@"getClusterChildren"];
}
-RCT_EXPORT_METHOD(getClusterLeaves:(nonnull NSNumber *)viewRef featureJSON:(NSString *)featureJSON number:(double)number offset:(double)offset resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
+RCT_EXPORT_METHOD(getClusterLeaves:(nonnull NSNumber *)viewRef featureJSON:(NSString *)featureJSON number:(long)number offset:(long)offset resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
[self withShapeSource:viewRef block:^(RNMBXShapeSource *view) {
[RNMBXShapeSourceViewManager getClusterLeavesWithShapeSource:view featureJSON:featureJSON number:number offset:offset resolver:resolve rejecter:reject];
diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceViewManager.swift b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceViewManager.swift
index 84f0407..f768c98 100644
--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceViewManager.swift
+++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXShapeSourceViewManager.swift
@@ -30,28 +30,28 @@ extension RNMBXShapeSourceViewManager {
}
}
- @objc public static func getClusterLeaves(
- shapeSource: RNMBXShapeSource,
- featureJSON: String,
- number: uint,
- offset: uint,
- resolver: @escaping RCTPromiseResolveBlock,
- rejecter: @escaping RCTPromiseRejectBlock) -> Void
- {
- shapeSource.getClusterLeaves(featureJSON, number: number, offset: offset) { result in
- switch result {
- case .success(let features):
- logged("getClusterLeaves", rejecter: rejecter) {
- let featuresJSON : Any = try features.features.toJSON()
- resolver([
- "data": ["type":"FeatureCollection", "features": featuresJSON]
- ])
- }
- case .failure(let error):
- rejecter(error.localizedDescription, "Error.getClusterLeaves", error)
+ @objc public static func getClusterLeaves(
+ shapeSource: RNMBXShapeSource,
+ featureJSON: String,
+ number: uint,
+ offset: uint,
+ resolver: @escaping RCTPromiseResolveBlock,
+ rejecter: @escaping RCTPromiseRejectBlock) -> Void
+ {
+ shapeSource.getClusterLeaves(featureJSON, number: 999, offset: offset) { result in
+ switch result {
+ case .success(let features):
+ logged("getClusterLeaves", rejecter: rejecter) {
+ let featuresJSON : Any = try features.features.toJSON()
+ resolver([
+ "data": ["type":"FeatureCollection", "features": featuresJSON]
+ ])
+ }
+ case .failure(let error):
+ rejecter(error.localizedDescription, "Error.getClusterLeaves", error)
+ }
}
- }
- }
+ }
@objc public static func getClusterChildren(
shapeSource: RNMBXShapeSource,
The issue is highly relevant when using "react-native": "0.77.2" together with the Magnus-V-main branch.
If you apply the fix as described here, it works correctly.
is it still broken in 10.1.39?
is it still broken in 10.1.39?
yes
Still broken in 10.1.40.
I've tried it in 1.2.5 and it worked
'visibleMarkers', [ { properties: { icon: 'example' },
id: 'a-feature',
geometry: { type: 'Point', coordinates: [ -74.00597, 40.71427 ] },
type: 'Feature' },
{ properties: { icon: 'example' },
id: 'b-feature',
geometry: { type: 'Point', coordinates: [ -74.001097, 40.71527 ] },
type: 'Feature' },
{ properties: { icon: 'example' },
id: 'c-feature',
geometry: { type: 'Point', coordinates: [ -74.00697, 40.72427 ] },
type: 'Feature' } ]